##// END OF EJS Templates
Merged r3305, r3306, r3307, r3311 from trunk....
Jean-Philippe Lang -
r3203:c5a59aff5b04
parent child
Show More
@@ -1,541 +1,541
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issue, :only => [:show, :edit, :reply]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
27 accept_key_auth :index, :show, :changes
27 accept_key_auth :index, :show, :changes
28
28
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30
30
31 helper :journals
31 helper :journals
32 helper :projects
32 helper :projects
33 include ProjectsHelper
33 include ProjectsHelper
34 helper :custom_fields
34 helper :custom_fields
35 include CustomFieldsHelper
35 include CustomFieldsHelper
36 helper :issue_relations
36 helper :issue_relations
37 include IssueRelationsHelper
37 include IssueRelationsHelper
38 helper :watchers
38 helper :watchers
39 include WatchersHelper
39 include WatchersHelper
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :queries
42 helper :queries
43 helper :sort
43 helper :sort
44 include SortHelper
44 include SortHelper
45 include IssuesHelper
45 include IssuesHelper
46 helper :timelog
46 helper :timelog
47 include Redmine::Export::PDF
47 include Redmine::Export::PDF
48
48
49 verify :method => :post,
49 verify :method => :post,
50 :only => :destroy,
50 :only => :destroy,
51 :render => { :nothing => true, :status => :method_not_allowed }
51 :render => { :nothing => true, :status => :method_not_allowed }
52
52
53 def index
53 def index
54 retrieve_query
54 retrieve_query
55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
56 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
56 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
57
57
58 if @query.valid?
58 if @query.valid?
59 limit = per_page_option
59 limit = per_page_option
60 respond_to do |format|
60 respond_to do |format|
61 format.html { }
61 format.html { }
62 format.atom { limit = Setting.feeds_limit.to_i }
62 format.atom { limit = Setting.feeds_limit.to_i }
63 format.csv { limit = Setting.issues_export_limit.to_i }
63 format.csv { limit = Setting.issues_export_limit.to_i }
64 format.pdf { limit = Setting.issues_export_limit.to_i }
64 format.pdf { limit = Setting.issues_export_limit.to_i }
65 end
65 end
66
66
67 @issue_count = @query.issue_count
67 @issue_count = @query.issue_count
68 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
68 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
69 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
69 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
70 :order => sort_clause,
70 :order => sort_clause,
71 :offset => @issue_pages.current.offset,
71 :offset => @issue_pages.current.offset,
72 :limit => limit)
72 :limit => limit)
73 @issue_count_by_group = @query.issue_count_by_group
73 @issue_count_by_group = @query.issue_count_by_group
74
74
75 respond_to do |format|
75 respond_to do |format|
76 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
76 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
77 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
77 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
78 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
78 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
79 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
79 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
80 end
80 end
81 else
81 else
82 # Send html if the query is not valid
82 # Send html if the query is not valid
83 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
83 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
84 end
84 end
85 rescue ActiveRecord::RecordNotFound
85 rescue ActiveRecord::RecordNotFound
86 render_404
86 render_404
87 end
87 end
88
88
89 def changes
89 def changes
90 retrieve_query
90 retrieve_query
91 sort_init 'id', 'desc'
91 sort_init 'id', 'desc'
92 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
92 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
93
93
94 if @query.valid?
94 if @query.valid?
95 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
95 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
96 :limit => 25)
96 :limit => 25)
97 end
97 end
98 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
98 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
99 render :layout => false, :content_type => 'application/atom+xml'
99 render :layout => false, :content_type => 'application/atom+xml'
100 rescue ActiveRecord::RecordNotFound
100 rescue ActiveRecord::RecordNotFound
101 render_404
101 render_404
102 end
102 end
103
103
104 def show
104 def show
105 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
105 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
106 @journals.each_with_index {|j,i| j.indice = i+1}
106 @journals.each_with_index {|j,i| j.indice = i+1}
107 @journals.reverse! if User.current.wants_comments_in_reverse_order?
107 @journals.reverse! if User.current.wants_comments_in_reverse_order?
108 @changesets = @issue.changesets
108 @changesets = @issue.changesets
109 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
109 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
111 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
111 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
112 @priorities = IssuePriority.all
112 @priorities = IssuePriority.all
113 @time_entry = TimeEntry.new
113 @time_entry = TimeEntry.new
114 respond_to do |format|
114 respond_to do |format|
115 format.html { render :template => 'issues/show.rhtml' }
115 format.html { render :template => 'issues/show.rhtml' }
116 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
116 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
117 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
117 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
118 end
118 end
119 end
119 end
120
120
121 # Add a new issue
121 # Add a new issue
122 # The new issue will be created from an existing one if copy_from parameter is given
122 # The new issue will be created from an existing one if copy_from parameter is given
123 def new
123 def new
124 @issue = Issue.new
124 @issue = Issue.new
125 @issue.copy_from(params[:copy_from]) if params[:copy_from]
125 @issue.copy_from(params[:copy_from]) if params[:copy_from]
126 @issue.project = @project
126 @issue.project = @project
127 # Tracker must be set before custom field values
127 # Tracker must be set before custom field values
128 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
128 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
129 if @issue.tracker.nil?
129 if @issue.tracker.nil?
130 render_error l(:error_no_tracker_in_project)
130 render_error l(:error_no_tracker_in_project)
131 return
131 return
132 end
132 end
133 if params[:issue].is_a?(Hash)
133 if params[:issue].is_a?(Hash)
134 @issue.attributes = params[:issue]
134 @issue.attributes = params[:issue]
135 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
135 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
136 end
136 end
137 @issue.author = User.current
137 @issue.author = User.current
138
138
139 default_status = IssueStatus.default
139 default_status = IssueStatus.default
140 unless default_status
140 unless default_status
141 render_error l(:error_no_default_issue_status)
141 render_error l(:error_no_default_issue_status)
142 return
142 return
143 end
143 end
144 @issue.status = default_status
144 @issue.status = default_status
145 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
145 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
146
146
147 if request.get? || request.xhr?
147 if request.get? || request.xhr?
148 @issue.start_date ||= Date.today
148 @issue.start_date ||= Date.today
149 else
149 else
150 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
150 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
151 # Check that the user is allowed to apply the requested status
151 # Check that the user is allowed to apply the requested status
152 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
152 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
153 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
153 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
154 if @issue.save
154 if @issue.save
155 attach_files(@issue, params[:attachments])
155 attach_files(@issue, params[:attachments])
156 flash[:notice] = l(:notice_successful_create)
156 flash[:notice] = l(:notice_successful_create)
157 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
157 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
158 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
158 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
159 { :action => 'show', :id => @issue })
159 { :action => 'show', :id => @issue })
160 return
160 return
161 end
161 end
162 end
162 end
163 @priorities = IssuePriority.all
163 @priorities = IssuePriority.all
164 render :layout => !request.xhr?
164 render :layout => !request.xhr?
165 end
165 end
166
166
167 # Attributes that can be updated on workflow transition (without :edit permission)
167 # Attributes that can be updated on workflow transition (without :edit permission)
168 # TODO: make it configurable (at least per role)
168 # TODO: make it configurable (at least per role)
169 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
169 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
170
170
171 def edit
171 def edit
172 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
172 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
173 @priorities = IssuePriority.all
173 @priorities = IssuePriority.all
174 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
174 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
175 @time_entry = TimeEntry.new
175 @time_entry = TimeEntry.new
176
176
177 @notes = params[:notes]
177 @notes = params[:notes]
178 journal = @issue.init_journal(User.current, @notes)
178 journal = @issue.init_journal(User.current, @notes)
179 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
179 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
180 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
180 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
181 attrs = params[:issue].dup
181 attrs = params[:issue].dup
182 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
182 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
183 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
183 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
184 @issue.attributes = attrs
184 @issue.attributes = attrs
185 end
185 end
186
186
187 if request.post?
187 if request.post?
188 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
188 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
189 @time_entry.attributes = params[:time_entry]
189 @time_entry.attributes = params[:time_entry]
190 attachments = attach_files(@issue, params[:attachments])
190 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
191 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
191 attachments = attach_files(@issue, params[:attachments])
192
192 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
193 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
193 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
194
194 if @issue.save
195 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
195 # Log spend time
196 # Log spend time
196 if User.current.allowed_to?(:log_time, @project)
197 if User.current.allowed_to?(:log_time, @project)
197 @time_entry.save
198 @time_entry.save
198 end
199 end
199 if !journal.new_record?
200 if !journal.new_record?
200 # Only send notification if something was actually changed
201 # Only send notification if something was actually changed
201 flash[:notice] = l(:notice_successful_update)
202 flash[:notice] = l(:notice_successful_update)
202 end
203 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
204 redirect_back_or_default({:action => 'show', :id => @issue})
203 end
205 end
204 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
205 redirect_back_or_default({:action => 'show', :id => @issue})
206 end
206 end
207 end
207 end
208 rescue ActiveRecord::StaleObjectError
208 rescue ActiveRecord::StaleObjectError
209 # Optimistic locking exception
209 # Optimistic locking exception
210 flash.now[:error] = l(:notice_locking_conflict)
210 flash.now[:error] = l(:notice_locking_conflict)
211 # Remove the previously added attachments if issue was not updated
211 # Remove the previously added attachments if issue was not updated
212 attachments.each(&:destroy)
212 attachments.each(&:destroy)
213 end
213 end
214
214
215 def reply
215 def reply
216 journal = Journal.find(params[:journal_id]) if params[:journal_id]
216 journal = Journal.find(params[:journal_id]) if params[:journal_id]
217 if journal
217 if journal
218 user = journal.user
218 user = journal.user
219 text = journal.notes
219 text = journal.notes
220 else
220 else
221 user = @issue.author
221 user = @issue.author
222 text = @issue.description
222 text = @issue.description
223 end
223 end
224 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
224 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
225 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
225 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
226 render(:update) { |page|
226 render(:update) { |page|
227 page.<< "$('notes').value = \"#{content}\";"
227 page.<< "$('notes').value = \"#{content}\";"
228 page.show 'update'
228 page.show 'update'
229 page << "Form.Element.focus('notes');"
229 page << "Form.Element.focus('notes');"
230 page << "Element.scrollTo('update');"
230 page << "Element.scrollTo('update');"
231 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
231 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
232 }
232 }
233 end
233 end
234
234
235 # Bulk edit a set of issues
235 # Bulk edit a set of issues
236 def bulk_edit
236 def bulk_edit
237 if request.post?
237 if request.post?
238 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
238 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
239 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
239 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
240 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
240 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
241 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
241 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
242 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
242 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
243 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
243 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
244 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
244 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
245
245
246 unsaved_issue_ids = []
246 unsaved_issue_ids = []
247 @issues.each do |issue|
247 @issues.each do |issue|
248 journal = issue.init_journal(User.current, params[:notes])
248 journal = issue.init_journal(User.current, params[:notes])
249 issue.tracker = tracker if tracker
249 issue.tracker = tracker if tracker
250 issue.priority = priority if priority
250 issue.priority = priority if priority
251 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
251 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
252 issue.category = category if category || params[:category_id] == 'none'
252 issue.category = category if category || params[:category_id] == 'none'
253 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
253 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
254 issue.start_date = params[:start_date] unless params[:start_date].blank?
254 issue.start_date = params[:start_date] unless params[:start_date].blank?
255 issue.due_date = params[:due_date] unless params[:due_date].blank?
255 issue.due_date = params[:due_date] unless params[:due_date].blank?
256 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
256 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
257 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
257 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
258 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
258 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
259 # Don't save any change to the issue if the user is not authorized to apply the requested status
259 # Don't save any change to the issue if the user is not authorized to apply the requested status
260 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
260 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
261 # Keep unsaved issue ids to display them in flash error
261 # Keep unsaved issue ids to display them in flash error
262 unsaved_issue_ids << issue.id
262 unsaved_issue_ids << issue.id
263 end
263 end
264 end
264 end
265 if unsaved_issue_ids.empty?
265 if unsaved_issue_ids.empty?
266 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
266 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
267 else
267 else
268 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
268 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
269 :total => @issues.size,
269 :total => @issues.size,
270 :ids => '#' + unsaved_issue_ids.join(', #'))
270 :ids => '#' + unsaved_issue_ids.join(', #'))
271 end
271 end
272 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
272 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
273 return
273 return
274 end
274 end
275 @available_statuses = Workflow.available_statuses(@project)
275 @available_statuses = Workflow.available_statuses(@project)
276 @custom_fields = @project.all_issue_custom_fields
276 @custom_fields = @project.all_issue_custom_fields
277 end
277 end
278
278
279 def move
279 def move
280 @copy = params[:copy_options] && params[:copy_options][:copy]
280 @copy = params[:copy_options] && params[:copy_options][:copy]
281 @allowed_projects = []
281 @allowed_projects = []
282 # find projects to which the user is allowed to move the issue
282 # find projects to which the user is allowed to move the issue
283 if User.current.admin?
283 if User.current.admin?
284 # admin is allowed to move issues to any active (visible) project
284 # admin is allowed to move issues to any active (visible) project
285 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
285 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
286 else
286 else
287 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
287 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
288 end
288 end
289 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
289 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
290 @target_project ||= @project
290 @target_project ||= @project
291 @trackers = @target_project.trackers
291 @trackers = @target_project.trackers
292 @available_statuses = Workflow.available_statuses(@project)
292 @available_statuses = Workflow.available_statuses(@project)
293 if request.post?
293 if request.post?
294 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
294 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
295 unsaved_issue_ids = []
295 unsaved_issue_ids = []
296 moved_issues = []
296 moved_issues = []
297 @issues.each do |issue|
297 @issues.each do |issue|
298 changed_attributes = {}
298 changed_attributes = {}
299 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
299 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
300 unless params[valid_attribute].blank?
300 unless params[valid_attribute].blank?
301 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
301 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
302 end
302 end
303 end
303 end
304 issue.init_journal(User.current)
304 issue.init_journal(User.current)
305 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
305 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
306 moved_issues << r
306 moved_issues << r
307 else
307 else
308 unsaved_issue_ids << issue.id
308 unsaved_issue_ids << issue.id
309 end
309 end
310 end
310 end
311 if unsaved_issue_ids.empty?
311 if unsaved_issue_ids.empty?
312 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
312 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
313 else
313 else
314 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
314 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
315 :total => @issues.size,
315 :total => @issues.size,
316 :ids => '#' + unsaved_issue_ids.join(', #'))
316 :ids => '#' + unsaved_issue_ids.join(', #'))
317 end
317 end
318 if params[:follow]
318 if params[:follow]
319 if @issues.size == 1 && moved_issues.size == 1
319 if @issues.size == 1 && moved_issues.size == 1
320 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
320 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
321 else
321 else
322 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
322 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
323 end
323 end
324 else
324 else
325 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
325 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
326 end
326 end
327 return
327 return
328 end
328 end
329 render :layout => false if request.xhr?
329 render :layout => false if request.xhr?
330 end
330 end
331
331
332 def destroy
332 def destroy
333 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
333 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
334 if @hours > 0
334 if @hours > 0
335 case params[:todo]
335 case params[:todo]
336 when 'destroy'
336 when 'destroy'
337 # nothing to do
337 # nothing to do
338 when 'nullify'
338 when 'nullify'
339 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
339 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
340 when 'reassign'
340 when 'reassign'
341 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
341 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
342 if reassign_to.nil?
342 if reassign_to.nil?
343 flash.now[:error] = l(:error_issue_not_found_in_project)
343 flash.now[:error] = l(:error_issue_not_found_in_project)
344 return
344 return
345 else
345 else
346 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
346 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
347 end
347 end
348 else
348 else
349 # display the destroy form
349 # display the destroy form
350 return
350 return
351 end
351 end
352 end
352 end
353 @issues.each(&:destroy)
353 @issues.each(&:destroy)
354 redirect_to :action => 'index', :project_id => @project
354 redirect_to :action => 'index', :project_id => @project
355 end
355 end
356
356
357 def gantt
357 def gantt
358 @gantt = Redmine::Helpers::Gantt.new(params)
358 @gantt = Redmine::Helpers::Gantt.new(params)
359 retrieve_query
359 retrieve_query
360 if @query.valid?
360 if @query.valid?
361 events = []
361 events = []
362 # Issues that have start and due dates
362 # Issues that have start and due dates
363 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
363 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
364 :order => "start_date, due_date",
364 :order => "start_date, due_date",
365 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
365 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
366 )
366 )
367 # Issues that don't have a due date but that are assigned to a version with a date
367 # Issues that don't have a due date but that are assigned to a version with a date
368 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
368 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
369 :order => "start_date, effective_date",
369 :order => "start_date, effective_date",
370 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
370 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
371 )
371 )
372 # Versions
372 # Versions
373 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
373 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
374
374
375 @gantt.events = events
375 @gantt.events = events
376 end
376 end
377
377
378 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
378 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
379
379
380 respond_to do |format|
380 respond_to do |format|
381 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
381 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
382 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
382 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
383 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
383 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
384 end
384 end
385 end
385 end
386
386
387 def calendar
387 def calendar
388 if params[:year] and params[:year].to_i > 1900
388 if params[:year] and params[:year].to_i > 1900
389 @year = params[:year].to_i
389 @year = params[:year].to_i
390 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
390 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
391 @month = params[:month].to_i
391 @month = params[:month].to_i
392 end
392 end
393 end
393 end
394 @year ||= Date.today.year
394 @year ||= Date.today.year
395 @month ||= Date.today.month
395 @month ||= Date.today.month
396
396
397 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
397 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
398 retrieve_query
398 retrieve_query
399 if @query.valid?
399 if @query.valid?
400 events = []
400 events = []
401 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
401 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
402 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
402 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
403 )
403 )
404 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
404 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
405
405
406 @calendar.events = events
406 @calendar.events = events
407 end
407 end
408
408
409 render :layout => false if request.xhr?
409 render :layout => false if request.xhr?
410 end
410 end
411
411
412 def context_menu
412 def context_menu
413 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
413 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
414 if (@issues.size == 1)
414 if (@issues.size == 1)
415 @issue = @issues.first
415 @issue = @issues.first
416 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
416 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
417 end
417 end
418 projects = @issues.collect(&:project).compact.uniq
418 projects = @issues.collect(&:project).compact.uniq
419 @project = projects.first if projects.size == 1
419 @project = projects.first if projects.size == 1
420
420
421 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
421 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
422 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
422 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
423 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
423 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
424 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
424 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
425 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
425 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
426 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
426 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
427 }
427 }
428 if @project
428 if @project
429 @assignables = @project.assignable_users
429 @assignables = @project.assignable_users
430 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
430 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
431 @trackers = @project.trackers
431 @trackers = @project.trackers
432 end
432 end
433
433
434 @priorities = IssuePriority.all.reverse
434 @priorities = IssuePriority.all.reverse
435 @statuses = IssueStatus.find(:all, :order => 'position')
435 @statuses = IssueStatus.find(:all, :order => 'position')
436 @back = params[:back_url] || request.env['HTTP_REFERER']
436 @back = params[:back_url] || request.env['HTTP_REFERER']
437
437
438 render :layout => false
438 render :layout => false
439 end
439 end
440
440
441 def update_form
441 def update_form
442 if params[:id].blank?
442 if params[:id].blank?
443 @issue = Issue.new
443 @issue = Issue.new
444 @issue.project = @project
444 @issue.project = @project
445 else
445 else
446 @issue = @project.issues.visible.find(params[:id])
446 @issue = @project.issues.visible.find(params[:id])
447 end
447 end
448 @issue.attributes = params[:issue]
448 @issue.attributes = params[:issue]
449 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
449 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
450 @priorities = IssuePriority.all
450 @priorities = IssuePriority.all
451
451
452 render :partial => 'attributes'
452 render :partial => 'attributes'
453 end
453 end
454
454
455 def preview
455 def preview
456 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
456 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
457 @attachements = @issue.attachments if @issue
457 @attachements = @issue.attachments if @issue
458 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
458 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
459 render :partial => 'common/preview'
459 render :partial => 'common/preview'
460 end
460 end
461
461
462 private
462 private
463 def find_issue
463 def find_issue
464 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
464 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
465 @project = @issue.project
465 @project = @issue.project
466 rescue ActiveRecord::RecordNotFound
466 rescue ActiveRecord::RecordNotFound
467 render_404
467 render_404
468 end
468 end
469
469
470 # Filter for bulk operations
470 # Filter for bulk operations
471 def find_issues
471 def find_issues
472 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
472 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
473 raise ActiveRecord::RecordNotFound if @issues.empty?
473 raise ActiveRecord::RecordNotFound if @issues.empty?
474 projects = @issues.collect(&:project).compact.uniq
474 projects = @issues.collect(&:project).compact.uniq
475 if projects.size == 1
475 if projects.size == 1
476 @project = projects.first
476 @project = projects.first
477 else
477 else
478 # TODO: let users bulk edit/move/destroy issues from different projects
478 # TODO: let users bulk edit/move/destroy issues from different projects
479 render_error 'Can not bulk edit/move/destroy issues from different projects'
479 render_error 'Can not bulk edit/move/destroy issues from different projects'
480 return false
480 return false
481 end
481 end
482 rescue ActiveRecord::RecordNotFound
482 rescue ActiveRecord::RecordNotFound
483 render_404
483 render_404
484 end
484 end
485
485
486 def find_project
486 def find_project
487 @project = Project.find(params[:project_id])
487 @project = Project.find(params[:project_id])
488 rescue ActiveRecord::RecordNotFound
488 rescue ActiveRecord::RecordNotFound
489 render_404
489 render_404
490 end
490 end
491
491
492 def find_optional_project
492 def find_optional_project
493 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
493 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
494 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
494 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
495 allowed ? true : deny_access
495 allowed ? true : deny_access
496 rescue ActiveRecord::RecordNotFound
496 rescue ActiveRecord::RecordNotFound
497 render_404
497 render_404
498 end
498 end
499
499
500 # Retrieve query from session or build a new query
500 # Retrieve query from session or build a new query
501 def retrieve_query
501 def retrieve_query
502 if !params[:query_id].blank?
502 if !params[:query_id].blank?
503 cond = "project_id IS NULL"
503 cond = "project_id IS NULL"
504 cond << " OR project_id = #{@project.id}" if @project
504 cond << " OR project_id = #{@project.id}" if @project
505 @query = Query.find(params[:query_id], :conditions => cond)
505 @query = Query.find(params[:query_id], :conditions => cond)
506 @query.project = @project
506 @query.project = @project
507 session[:query] = {:id => @query.id, :project_id => @query.project_id}
507 session[:query] = {:id => @query.id, :project_id => @query.project_id}
508 sort_clear
508 sort_clear
509 else
509 else
510 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
510 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
511 # Give it a name, required to be valid
511 # Give it a name, required to be valid
512 @query = Query.new(:name => "_")
512 @query = Query.new(:name => "_")
513 @query.project = @project
513 @query.project = @project
514 if params[:fields] and params[:fields].is_a? Array
514 if params[:fields] and params[:fields].is_a? Array
515 params[:fields].each do |field|
515 params[:fields].each do |field|
516 @query.add_filter(field, params[:operators][field], params[:values][field])
516 @query.add_filter(field, params[:operators][field], params[:values][field])
517 end
517 end
518 else
518 else
519 @query.available_filters.keys.each do |field|
519 @query.available_filters.keys.each do |field|
520 @query.add_short_filter(field, params[field]) if params[field]
520 @query.add_short_filter(field, params[field]) if params[field]
521 end
521 end
522 end
522 end
523 @query.group_by = params[:group_by]
523 @query.group_by = params[:group_by]
524 @query.column_names = params[:query] && params[:query][:column_names]
524 @query.column_names = params[:query] && params[:query][:column_names]
525 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
525 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
526 else
526 else
527 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
527 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
528 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
528 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
529 @query.project = @project
529 @query.project = @project
530 end
530 end
531 end
531 end
532 end
532 end
533
533
534 # Rescues an invalid query statement. Just in case...
534 # Rescues an invalid query statement. Just in case...
535 def query_statement_invalid(exception)
535 def query_statement_invalid(exception)
536 logger.error "Query::StatementInvalid: #{exception.message}" if logger
536 logger.error "Query::StatementInvalid: #{exception.message}" if logger
537 session.delete(:query)
537 session.delete(:query)
538 sort_clear
538 sort_clear
539 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
539 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
540 end
540 end
541 end
541 end
@@ -1,116 +1,116
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 SearchController < ApplicationController
18 class SearchController < ApplicationController
19 before_filter :find_optional_project
19 before_filter :find_optional_project
20
20
21 helper :messages
21 helper :messages
22 include MessagesHelper
22 include MessagesHelper
23
23
24 def index
24 def index
25 @question = params[:q] || ""
25 @question = params[:q] || ""
26 @question.strip!
26 @question.strip!
27 @all_words = params[:all_words] || (params[:submit] ? false : true)
27 @all_words = params[:all_words] || (params[:submit] ? false : true)
28 @titles_only = !params[:titles_only].nil?
28 @titles_only = !params[:titles_only].nil?
29
29
30 projects_to_search =
30 projects_to_search =
31 case params[:scope]
31 case params[:scope]
32 when 'all'
32 when 'all'
33 nil
33 nil
34 when 'my_projects'
34 when 'my_projects'
35 User.current.memberships.collect(&:project)
35 User.current.memberships.collect(&:project)
36 when 'subprojects'
36 when 'subprojects'
37 @project ? (@project.self_and_descendants.active) : nil
37 @project ? (@project.self_and_descendants.active) : nil
38 else
38 else
39 @project
39 @project
40 end
40 end
41
41
42 offset = nil
42 offset = nil
43 begin; offset = params[:offset].to_time if params[:offset]; rescue; end
43 begin; offset = params[:offset].to_time if params[:offset]; rescue; end
44
44
45 # quick jump to an issue
45 # quick jump to an issue
46 if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1)
46 if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1)
47 redirect_to :controller => "issues", :action => "show", :id => $1
47 redirect_to :controller => "issues", :action => "show", :id => $1
48 return
48 return
49 end
49 end
50
50
51 @object_types = %w(issues news documents changesets wiki_pages messages projects)
51 @object_types = %w(issues news documents changesets wiki_pages messages projects)
52 if projects_to_search.is_a? Project
52 if projects_to_search.is_a? Project
53 # don't search projects
53 # don't search projects
54 @object_types.delete('projects')
54 @object_types.delete('projects')
55 # only show what the user is allowed to view
55 # only show what the user is allowed to view
56 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
56 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
57 end
57 end
58
58
59 @scope = @object_types.select {|t| params[t]}
59 @scope = @object_types.select {|t| params[t]}
60 @scope = @object_types if @scope.empty?
60 @scope = @object_types if @scope.empty?
61
61
62 # extract tokens from the question
62 # extract tokens from the question
63 # eg. hello "bye bye" => ["hello", "bye bye"]
63 # eg. hello "bye bye" => ["hello", "bye bye"]
64 @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
64 @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
65 # tokens must be at least 3 character long
65 # tokens must be at least 2 characters long
66 @tokens = @tokens.uniq.select {|w| w.length > 2 }
66 @tokens = @tokens.uniq.select {|w| w.length > 1 }
67
67
68 if !@tokens.empty?
68 if !@tokens.empty?
69 # no more than 5 tokens to search for
69 # no more than 5 tokens to search for
70 @tokens.slice! 5..-1 if @tokens.size > 5
70 @tokens.slice! 5..-1 if @tokens.size > 5
71 # strings used in sql like statement
71 # strings used in sql like statement
72 like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
72 like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
73
73
74 @results = []
74 @results = []
75 @results_by_type = Hash.new {|h,k| h[k] = 0}
75 @results_by_type = Hash.new {|h,k| h[k] = 0}
76
76
77 limit = 10
77 limit = 10
78 @scope.each do |s|
78 @scope.each do |s|
79 r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
79 r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
80 :all_words => @all_words,
80 :all_words => @all_words,
81 :titles_only => @titles_only,
81 :titles_only => @titles_only,
82 :limit => (limit+1),
82 :limit => (limit+1),
83 :offset => offset,
83 :offset => offset,
84 :before => params[:previous].nil?)
84 :before => params[:previous].nil?)
85 @results += r
85 @results += r
86 @results_by_type[s] += c
86 @results_by_type[s] += c
87 end
87 end
88 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
88 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
89 if params[:previous].nil?
89 if params[:previous].nil?
90 @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
90 @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
91 if @results.size > limit
91 if @results.size > limit
92 @pagination_next_date = @results[limit-1].event_datetime
92 @pagination_next_date = @results[limit-1].event_datetime
93 @results = @results[0, limit]
93 @results = @results[0, limit]
94 end
94 end
95 else
95 else
96 @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
96 @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
97 if @results.size > limit
97 if @results.size > limit
98 @pagination_previous_date = @results[-(limit)].event_datetime
98 @pagination_previous_date = @results[-(limit)].event_datetime
99 @results = @results[-(limit), limit]
99 @results = @results[-(limit), limit]
100 end
100 end
101 end
101 end
102 else
102 else
103 @question = ""
103 @question = ""
104 end
104 end
105 render :layout => false if request.xhr?
105 render :layout => false if request.xhr?
106 end
106 end
107
107
108 private
108 private
109 def find_optional_project
109 def find_optional_project
110 return true unless params[:id]
110 return true unless params[:id]
111 @project = Project.find(params[:id])
111 @project = Project.find(params[:id])
112 check_project_privacy
112 check_project_privacy
113 rescue ActiveRecord::RecordNotFound
113 rescue ActiveRecord::RecordNotFound
114 render_404
114 render_404
115 end
115 end
116 end
116 end
@@ -1,199 +1,199
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 module IssuesHelper
18 module IssuesHelper
19 include ApplicationHelper
19 include ApplicationHelper
20
20
21 def render_issue_tooltip(issue)
21 def render_issue_tooltip(issue)
22 @cached_label_start_date ||= l(:field_start_date)
22 @cached_label_start_date ||= l(:field_start_date)
23 @cached_label_due_date ||= l(:field_due_date)
23 @cached_label_due_date ||= l(:field_due_date)
24 @cached_label_assigned_to ||= l(:field_assigned_to)
24 @cached_label_assigned_to ||= l(:field_assigned_to)
25 @cached_label_priority ||= l(:field_priority)
25 @cached_label_priority ||= l(:field_priority)
26
26
27 link_to_issue(issue) + "<br /><br />" +
27 link_to_issue(issue) + "<br /><br />" +
28 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
28 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
29 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
29 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
30 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
30 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
31 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
31 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
32 end
32 end
33
33
34 def render_custom_fields_rows(issue)
34 def render_custom_fields_rows(issue)
35 return if issue.custom_field_values.empty?
35 return if issue.custom_field_values.empty?
36 ordered_values = []
36 ordered_values = []
37 half = (issue.custom_field_values.size / 2.0).ceil
37 half = (issue.custom_field_values.size / 2.0).ceil
38 half.times do |i|
38 half.times do |i|
39 ordered_values << issue.custom_field_values[i]
39 ordered_values << issue.custom_field_values[i]
40 ordered_values << issue.custom_field_values[i + half]
40 ordered_values << issue.custom_field_values[i + half]
41 end
41 end
42 s = "<tr>\n"
42 s = "<tr>\n"
43 n = 0
43 n = 0
44 ordered_values.compact.each do |value|
44 ordered_values.compact.each do |value|
45 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
45 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
46 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
46 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
47 n += 1
47 n += 1
48 end
48 end
49 s << "</tr>\n"
49 s << "</tr>\n"
50 s
50 s
51 end
51 end
52
52
53 def sidebar_queries
53 def sidebar_queries
54 unless @sidebar_queries
54 unless @sidebar_queries
55 # User can see public queries and his own queries
55 # User can see public queries and his own queries
56 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
56 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
57 # Project specific queries and global queries
57 # Project specific queries and global queries
58 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
58 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
59 @sidebar_queries = Query.find(:all,
59 @sidebar_queries = Query.find(:all,
60 :select => 'id, name',
60 :select => 'id, name',
61 :order => "name ASC",
61 :order => "name ASC",
62 :conditions => visible.conditions)
62 :conditions => visible.conditions)
63 end
63 end
64 @sidebar_queries
64 @sidebar_queries
65 end
65 end
66
66
67 def show_detail(detail, no_html=false)
67 def show_detail(detail, no_html=false)
68 case detail.property
68 case detail.property
69 when 'attr'
69 when 'attr'
70 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
70 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
71 case detail.prop_key
71 case detail.prop_key
72 when 'due_date', 'start_date'
72 when 'due_date', 'start_date'
73 value = format_date(detail.value.to_date) if detail.value
73 value = format_date(detail.value.to_date) if detail.value
74 old_value = format_date(detail.old_value.to_date) if detail.old_value
74 old_value = format_date(detail.old_value.to_date) if detail.old_value
75 when 'project_id'
75 when 'project_id'
76 p = Project.find_by_id(detail.value) and value = p.name if detail.value
76 p = Project.find_by_id(detail.value) and value = p.name if detail.value
77 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
77 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
78 when 'status_id'
78 when 'status_id'
79 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
79 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
80 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
80 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
81 when 'tracker_id'
81 when 'tracker_id'
82 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
82 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
83 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
83 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
84 when 'assigned_to_id'
84 when 'assigned_to_id'
85 u = User.find_by_id(detail.value) and value = u.name if detail.value
85 u = User.find_by_id(detail.value) and value = u.name if detail.value
86 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
86 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
87 when 'priority_id'
87 when 'priority_id'
88 e = IssuePriority.find_by_id(detail.value) and value = e.name if detail.value
88 e = IssuePriority.find_by_id(detail.value) and value = e.name if detail.value
89 e = IssuePriority.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
89 e = IssuePriority.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
90 when 'category_id'
90 when 'category_id'
91 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
91 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
92 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
92 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
93 when 'fixed_version_id'
93 when 'fixed_version_id'
94 v = Version.find_by_id(detail.value) and value = format_version_name(v) if detail.value
94 v = Version.find_by_id(detail.value) and value = v.name if detail.value
95 v = Version.find_by_id(detail.old_value) and old_value = format_version_name(v) if detail.old_value
95 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
96 when 'estimated_hours'
96 when 'estimated_hours'
97 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
97 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
98 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
98 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
99 end
99 end
100 when 'cf'
100 when 'cf'
101 custom_field = CustomField.find_by_id(detail.prop_key)
101 custom_field = CustomField.find_by_id(detail.prop_key)
102 if custom_field
102 if custom_field
103 label = custom_field.name
103 label = custom_field.name
104 value = format_value(detail.value, custom_field.field_format) if detail.value
104 value = format_value(detail.value, custom_field.field_format) if detail.value
105 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
105 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
106 end
106 end
107 when 'attachment'
107 when 'attachment'
108 label = l(:label_attachment)
108 label = l(:label_attachment)
109 end
109 end
110 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
110 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
111
111
112 label ||= detail.prop_key
112 label ||= detail.prop_key
113 value ||= detail.value
113 value ||= detail.value
114 old_value ||= detail.old_value
114 old_value ||= detail.old_value
115
115
116 unless no_html
116 unless no_html
117 label = content_tag('strong', label)
117 label = content_tag('strong', label)
118 old_value = content_tag("i", h(old_value)) if detail.old_value
118 old_value = content_tag("i", h(old_value)) if detail.old_value
119 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
119 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
120 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
120 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
121 # Link to the attachment if it has not been removed
121 # Link to the attachment if it has not been removed
122 value = link_to_attachment(a)
122 value = link_to_attachment(a)
123 else
123 else
124 value = content_tag("i", h(value)) if value
124 value = content_tag("i", h(value)) if value
125 end
125 end
126 end
126 end
127
127
128 if !detail.value.blank?
128 if !detail.value.blank?
129 case detail.property
129 case detail.property
130 when 'attr', 'cf'
130 when 'attr', 'cf'
131 if !detail.old_value.blank?
131 if !detail.old_value.blank?
132 l(:text_journal_changed, :label => label, :old => old_value, :new => value)
132 l(:text_journal_changed, :label => label, :old => old_value, :new => value)
133 else
133 else
134 l(:text_journal_set_to, :label => label, :value => value)
134 l(:text_journal_set_to, :label => label, :value => value)
135 end
135 end
136 when 'attachment'
136 when 'attachment'
137 l(:text_journal_added, :label => label, :value => value)
137 l(:text_journal_added, :label => label, :value => value)
138 end
138 end
139 else
139 else
140 l(:text_journal_deleted, :label => label, :old => old_value)
140 l(:text_journal_deleted, :label => label, :old => old_value)
141 end
141 end
142 end
142 end
143
143
144 def issues_to_csv(issues, project = nil)
144 def issues_to_csv(issues, project = nil)
145 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
145 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
146 decimal_separator = l(:general_csv_decimal_separator)
146 decimal_separator = l(:general_csv_decimal_separator)
147 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
147 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
148 # csv header fields
148 # csv header fields
149 headers = [ "#",
149 headers = [ "#",
150 l(:field_status),
150 l(:field_status),
151 l(:field_project),
151 l(:field_project),
152 l(:field_tracker),
152 l(:field_tracker),
153 l(:field_priority),
153 l(:field_priority),
154 l(:field_subject),
154 l(:field_subject),
155 l(:field_assigned_to),
155 l(:field_assigned_to),
156 l(:field_category),
156 l(:field_category),
157 l(:field_fixed_version),
157 l(:field_fixed_version),
158 l(:field_author),
158 l(:field_author),
159 l(:field_start_date),
159 l(:field_start_date),
160 l(:field_due_date),
160 l(:field_due_date),
161 l(:field_done_ratio),
161 l(:field_done_ratio),
162 l(:field_estimated_hours),
162 l(:field_estimated_hours),
163 l(:field_created_on),
163 l(:field_created_on),
164 l(:field_updated_on)
164 l(:field_updated_on)
165 ]
165 ]
166 # Export project custom fields if project is given
166 # Export project custom fields if project is given
167 # otherwise export custom fields marked as "For all projects"
167 # otherwise export custom fields marked as "For all projects"
168 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
168 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
169 custom_fields.each {|f| headers << f.name}
169 custom_fields.each {|f| headers << f.name}
170 # Description in the last column
170 # Description in the last column
171 headers << l(:field_description)
171 headers << l(:field_description)
172 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
172 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
173 # csv lines
173 # csv lines
174 issues.each do |issue|
174 issues.each do |issue|
175 fields = [issue.id,
175 fields = [issue.id,
176 issue.status.name,
176 issue.status.name,
177 issue.project.name,
177 issue.project.name,
178 issue.tracker.name,
178 issue.tracker.name,
179 issue.priority.name,
179 issue.priority.name,
180 issue.subject,
180 issue.subject,
181 issue.assigned_to,
181 issue.assigned_to,
182 issue.category,
182 issue.category,
183 issue.fixed_version,
183 issue.fixed_version,
184 issue.author.name,
184 issue.author.name,
185 format_date(issue.start_date),
185 format_date(issue.start_date),
186 format_date(issue.due_date),
186 format_date(issue.due_date),
187 issue.done_ratio,
187 issue.done_ratio,
188 issue.estimated_hours.to_s.gsub('.', decimal_separator),
188 issue.estimated_hours.to_s.gsub('.', decimal_separator),
189 format_time(issue.created_on),
189 format_time(issue.created_on),
190 format_time(issue.updated_on)
190 format_time(issue.updated_on)
191 ]
191 ]
192 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
192 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
193 fields << issue.description
193 fields << issue.description
194 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
194 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
195 end
195 end
196 end
196 end
197 export
197 export
198 end
198 end
199 end
199 end
@@ -1,403 +1,405
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 ActionController::UrlWriter
24 include ActionController::UrlWriter
25 include Redmine::I18n
25 include Redmine::I18n
26
26
27 def self.default_url_options
27 def self.default_url_options
28 h = Setting.host_name
28 h = Setting.host_name
29 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
29 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
30 { :host => h, :protocol => Setting.protocol }
30 { :host => h, :protocol => Setting.protocol }
31 end
31 end
32
32
33 # Builds a tmail object used to email recipients of the added issue.
33 # Builds a tmail object used to email recipients of the added issue.
34 #
34 #
35 # Example:
35 # Example:
36 # issue_add(issue) => tmail object
36 # issue_add(issue) => tmail object
37 # Mailer.deliver_issue_add(issue) => sends an email to issue recipients
37 # Mailer.deliver_issue_add(issue) => sends an email to issue recipients
38 def issue_add(issue)
38 def issue_add(issue)
39 redmine_headers 'Project' => issue.project.identifier,
39 redmine_headers 'Project' => issue.project.identifier,
40 'Issue-Id' => issue.id,
40 'Issue-Id' => issue.id,
41 'Issue-Author' => issue.author.login
41 'Issue-Author' => issue.author.login
42 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
42 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
43 message_id issue
43 message_id issue
44 recipients issue.recipients
44 recipients issue.recipients
45 cc(issue.watcher_recipients - @recipients)
45 cc(issue.watcher_recipients - @recipients)
46 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
46 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
47 body :issue => issue,
47 body :issue => issue,
48 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
48 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
49 render_multipart('issue_add', body)
49 render_multipart('issue_add', body)
50 end
50 end
51
51
52 # Builds a tmail object used to email recipients of the edited issue.
52 # Builds a tmail object used to email recipients of the edited issue.
53 #
53 #
54 # Example:
54 # Example:
55 # issue_edit(journal) => tmail object
55 # issue_edit(journal) => tmail object
56 # Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
56 # Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
57 def issue_edit(journal)
57 def issue_edit(journal)
58 issue = journal.journalized.reload
58 issue = journal.journalized.reload
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 recipients issue.recipients
66 recipients issue.recipients
67 # Watchers in cc
67 # Watchers in cc
68 cc(issue.watcher_recipients - @recipients)
68 cc(issue.watcher_recipients - @recipients)
69 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
69 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
70 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
70 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
71 s << issue.subject
71 s << issue.subject
72 subject s
72 subject s
73 body :issue => issue,
73 body :issue => issue,
74 :journal => journal,
74 :journal => journal,
75 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
75 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
76
76
77 render_multipart('issue_edit', body)
77 render_multipart('issue_edit', body)
78 end
78 end
79
79
80 def reminder(user, issues, days)
80 def reminder(user, issues, days)
81 set_language_if_valid user.language
81 set_language_if_valid user.language
82 recipients user.mail
82 recipients user.mail
83 subject l(:mail_subject_reminder, issues.size)
83 subject l(:mail_subject_reminder, issues.size)
84 body :issues => issues,
84 body :issues => issues,
85 :days => days,
85 :days => days,
86 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
86 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
87 render_multipart('reminder', body)
87 render_multipart('reminder', body)
88 end
88 end
89
89
90 # Builds a tmail object used to email users belonging to the added document's project.
90 # Builds a tmail object used to email users belonging to the added document's project.
91 #
91 #
92 # Example:
92 # Example:
93 # document_added(document) => tmail object
93 # document_added(document) => tmail object
94 # Mailer.deliver_document_added(document) => sends an email to the document's project recipients
94 # Mailer.deliver_document_added(document) => sends an email to the document's project recipients
95 def document_added(document)
95 def document_added(document)
96 redmine_headers 'Project' => document.project.identifier
96 redmine_headers 'Project' => document.project.identifier
97 recipients document.recipients
97 recipients document.recipients
98 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
98 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
99 body :document => document,
99 body :document => document,
100 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
100 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
101 render_multipart('document_added', body)
101 render_multipart('document_added', body)
102 end
102 end
103
103
104 # Builds a tmail object used to email recipients of a project when an attachements are added.
104 # Builds a tmail object used to email recipients of a project when an attachements are added.
105 #
105 #
106 # Example:
106 # Example:
107 # attachments_added(attachments) => tmail object
107 # attachments_added(attachments) => tmail object
108 # Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
108 # Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
109 def attachments_added(attachments)
109 def attachments_added(attachments)
110 container = attachments.first.container
110 container = attachments.first.container
111 added_to = ''
111 added_to = ''
112 added_to_url = ''
112 added_to_url = ''
113 case container.class.name
113 case container.class.name
114 when 'Project'
114 when 'Project'
115 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
115 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
116 added_to = "#{l(:label_project)}: #{container}"
116 added_to = "#{l(:label_project)}: #{container}"
117 recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
117 recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
118 when 'Version'
118 when 'Version'
119 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
119 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
120 added_to = "#{l(:label_version)}: #{container.name}"
120 added_to = "#{l(:label_version)}: #{container.name}"
121 recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
121 recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
122 when 'Document'
122 when 'Document'
123 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
123 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
124 added_to = "#{l(:label_document)}: #{container.title}"
124 added_to = "#{l(:label_document)}: #{container.title}"
125 recipients container.recipients
125 recipients container.recipients
126 end
126 end
127 redmine_headers 'Project' => container.project.identifier
127 redmine_headers 'Project' => container.project.identifier
128 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
128 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
129 body :attachments => attachments,
129 body :attachments => attachments,
130 :added_to => added_to,
130 :added_to => added_to,
131 :added_to_url => added_to_url
131 :added_to_url => added_to_url
132 render_multipart('attachments_added', body)
132 render_multipart('attachments_added', body)
133 end
133 end
134
134
135 # Builds a tmail object used to email recipients of a news' project when a news item is added.
135 # Builds a tmail object used to email recipients of a news' project when a news item is added.
136 #
136 #
137 # Example:
137 # Example:
138 # news_added(news) => tmail object
138 # news_added(news) => tmail object
139 # Mailer.deliver_news_added(news) => sends an email to the news' project recipients
139 # Mailer.deliver_news_added(news) => sends an email to the news' project recipients
140 def news_added(news)
140 def news_added(news)
141 redmine_headers 'Project' => news.project.identifier
141 redmine_headers 'Project' => news.project.identifier
142 message_id news
142 message_id news
143 recipients news.recipients
143 recipients news.recipients
144 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
144 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
145 body :news => news,
145 body :news => news,
146 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
146 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
147 render_multipart('news_added', body)
147 render_multipart('news_added', body)
148 end
148 end
149
149
150 # Builds a tmail object used to email the recipients of the specified message that was posted.
150 # Builds a tmail object used to email the recipients of the specified message that was posted.
151 #
151 #
152 # Example:
152 # Example:
153 # message_posted(message) => tmail object
153 # message_posted(message) => tmail object
154 # Mailer.deliver_message_posted(message) => sends an email to the recipients
154 # Mailer.deliver_message_posted(message) => sends an email to the recipients
155 def message_posted(message)
155 def message_posted(message)
156 redmine_headers 'Project' => message.project.identifier,
156 redmine_headers 'Project' => message.project.identifier,
157 'Topic-Id' => (message.parent_id || message.id)
157 'Topic-Id' => (message.parent_id || message.id)
158 message_id message
158 message_id message
159 references message.parent unless message.parent.nil?
159 references message.parent unless message.parent.nil?
160 recipients(message.recipients)
160 recipients(message.recipients)
161 cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
161 cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
162 subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
162 subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
163 body :message => message,
163 body :message => message,
164 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
164 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
165 render_multipart('message_posted', body)
165 render_multipart('message_posted', body)
166 end
166 end
167
167
168 # Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
168 # Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
169 #
169 #
170 # Example:
170 # Example:
171 # wiki_content_added(wiki_content) => tmail object
171 # wiki_content_added(wiki_content) => tmail object
172 # Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
172 # Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
173 def wiki_content_added(wiki_content)
173 def wiki_content_added(wiki_content)
174 redmine_headers 'Project' => wiki_content.project.identifier,
174 redmine_headers 'Project' => wiki_content.project.identifier,
175 'Wiki-Page-Id' => wiki_content.page.id
175 'Wiki-Page-Id' => wiki_content.page.id
176 message_id wiki_content
176 message_id wiki_content
177 recipients wiki_content.recipients
177 recipients wiki_content.recipients
178 cc(wiki_content.page.wiki.watcher_recipients - recipients)
178 cc(wiki_content.page.wiki.watcher_recipients - recipients)
179 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :page => wiki_content.page.pretty_title)}"
179 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :page => wiki_content.page.pretty_title)}"
180 body :wiki_content => wiki_content,
180 body :wiki_content => wiki_content,
181 :wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title)
181 :wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title)
182 render_multipart('wiki_content_added', body)
182 render_multipart('wiki_content_added', body)
183 end
183 end
184
184
185 # Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
185 # Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
186 #
186 #
187 # Example:
187 # Example:
188 # wiki_content_updated(wiki_content) => tmail object
188 # wiki_content_updated(wiki_content) => tmail object
189 # Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
189 # Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
190 def wiki_content_updated(wiki_content)
190 def wiki_content_updated(wiki_content)
191 redmine_headers 'Project' => wiki_content.project.identifier,
191 redmine_headers 'Project' => wiki_content.project.identifier,
192 'Wiki-Page-Id' => wiki_content.page.id
192 'Wiki-Page-Id' => wiki_content.page.id
193 message_id wiki_content
193 message_id wiki_content
194 recipients wiki_content.recipients
194 recipients wiki_content.recipients
195 cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
195 cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
196 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :page => wiki_content.page.pretty_title)}"
196 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :page => wiki_content.page.pretty_title)}"
197 body :wiki_content => wiki_content,
197 body :wiki_content => wiki_content,
198 :wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title),
198 :wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title),
199 :wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff', :id => wiki_content.project, :page => wiki_content.page.title, :version => wiki_content.version)
199 :wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff', :id => wiki_content.project, :page => wiki_content.page.title, :version => wiki_content.version)
200 render_multipart('wiki_content_updated', body)
200 render_multipart('wiki_content_updated', body)
201 end
201 end
202
202
203 # Builds a tmail object used to email the specified user their account information.
203 # Builds a tmail object used to email the specified user their account information.
204 #
204 #
205 # Example:
205 # Example:
206 # account_information(user, password) => tmail object
206 # account_information(user, password) => tmail object
207 # Mailer.deliver_account_information(user, password) => sends account information to the user
207 # Mailer.deliver_account_information(user, password) => sends account information to the user
208 def account_information(user, password)
208 def account_information(user, password)
209 set_language_if_valid user.language
209 set_language_if_valid user.language
210 recipients user.mail
210 recipients user.mail
211 subject l(:mail_subject_register, Setting.app_title)
211 subject l(:mail_subject_register, Setting.app_title)
212 body :user => user,
212 body :user => user,
213 :password => password,
213 :password => password,
214 :login_url => url_for(:controller => 'account', :action => 'login')
214 :login_url => url_for(:controller => 'account', :action => 'login')
215 render_multipart('account_information', body)
215 render_multipart('account_information', body)
216 end
216 end
217
217
218 # Builds a tmail object used to email all active administrators of an account activation request.
218 # Builds a tmail object used to email all active administrators of an account activation request.
219 #
219 #
220 # Example:
220 # Example:
221 # account_activation_request(user) => tmail object
221 # account_activation_request(user) => tmail object
222 # Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
222 # Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
223 def account_activation_request(user)
223 def account_activation_request(user)
224 # Send the email to all active administrators
224 # Send the email to all active administrators
225 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
225 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
226 subject l(:mail_subject_account_activation_request, Setting.app_title)
226 subject l(:mail_subject_account_activation_request, Setting.app_title)
227 body :user => user,
227 body :user => user,
228 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
228 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
229 render_multipart('account_activation_request', body)
229 render_multipart('account_activation_request', body)
230 end
230 end
231
231
232 # Builds a tmail object used to email the specified user that their account was activated by an administrator.
232 # Builds a tmail object used to email the specified user that their account was activated by an administrator.
233 #
233 #
234 # Example:
234 # Example:
235 # account_activated(user) => tmail object
235 # account_activated(user) => tmail object
236 # Mailer.deliver_account_activated(user) => sends an email to the registered user
236 # Mailer.deliver_account_activated(user) => sends an email to the registered user
237 def account_activated(user)
237 def account_activated(user)
238 set_language_if_valid user.language
238 set_language_if_valid user.language
239 recipients user.mail
239 recipients user.mail
240 subject l(:mail_subject_register, Setting.app_title)
240 subject l(:mail_subject_register, Setting.app_title)
241 body :user => user,
241 body :user => user,
242 :login_url => url_for(:controller => 'account', :action => 'login')
242 :login_url => url_for(:controller => 'account', :action => 'login')
243 render_multipart('account_activated', body)
243 render_multipart('account_activated', body)
244 end
244 end
245
245
246 def lost_password(token)
246 def lost_password(token)
247 set_language_if_valid(token.user.language)
247 set_language_if_valid(token.user.language)
248 recipients token.user.mail
248 recipients token.user.mail
249 subject l(:mail_subject_lost_password, Setting.app_title)
249 subject l(:mail_subject_lost_password, Setting.app_title)
250 body :token => token,
250 body :token => token,
251 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
251 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
252 render_multipart('lost_password', body)
252 render_multipart('lost_password', body)
253 end
253 end
254
254
255 def register(token)
255 def register(token)
256 set_language_if_valid(token.user.language)
256 set_language_if_valid(token.user.language)
257 recipients token.user.mail
257 recipients token.user.mail
258 subject l(:mail_subject_register, Setting.app_title)
258 subject l(:mail_subject_register, Setting.app_title)
259 body :token => token,
259 body :token => token,
260 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
260 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
261 render_multipart('register', body)
261 render_multipart('register', body)
262 end
262 end
263
263
264 def test(user)
264 def test(user)
265 set_language_if_valid(user.language)
265 set_language_if_valid(user.language)
266 recipients user.mail
266 recipients user.mail
267 subject 'Redmine test'
267 subject 'Redmine test'
268 body :url => url_for(:controller => 'welcome')
268 body :url => url_for(:controller => 'welcome')
269 render_multipart('test', body)
269 render_multipart('test', body)
270 end
270 end
271
271
272 # Overrides default deliver! method to prevent from sending an email
272 # Overrides default deliver! method to prevent from sending an email
273 # with no recipient, cc or bcc
273 # with no recipient, cc or bcc
274 def deliver!(mail = @mail)
274 def deliver!(mail = @mail)
275 set_language_if_valid @initial_language
275 return false if (recipients.nil? || recipients.empty?) &&
276 return false if (recipients.nil? || recipients.empty?) &&
276 (cc.nil? || cc.empty?) &&
277 (cc.nil? || cc.empty?) &&
277 (bcc.nil? || bcc.empty?)
278 (bcc.nil? || bcc.empty?)
278
279
279 # Set Message-Id and References
280 # Set Message-Id and References
280 if @message_id_object
281 if @message_id_object
281 mail.message_id = self.class.message_id_for(@message_id_object)
282 mail.message_id = self.class.message_id_for(@message_id_object)
282 end
283 end
283 if @references_objects
284 if @references_objects
284 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
285 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
285 end
286 end
286 super(mail)
287 super(mail)
287 end
288 end
288
289
289 # Sends reminders to issue assignees
290 # Sends reminders to issue assignees
290 # Available options:
291 # Available options:
291 # * :days => how many days in the future to remind about (defaults to 7)
292 # * :days => how many days in the future to remind about (defaults to 7)
292 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
293 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
293 # * :project => id or identifier of project to process (defaults to all projects)
294 # * :project => id or identifier of project to process (defaults to all projects)
294 def self.reminders(options={})
295 def self.reminders(options={})
295 days = options[:days] || 7
296 days = options[:days] || 7
296 project = options[:project] ? Project.find(options[:project]) : nil
297 project = options[:project] ? Project.find(options[:project]) : nil
297 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
298 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
298
299
299 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
300 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
300 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
301 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
301 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
302 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
302 s << "#{Issue.table_name}.project_id = #{project.id}" if project
303 s << "#{Issue.table_name}.project_id = #{project.id}" if project
303 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
304 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
304
305
305 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
306 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
306 :conditions => s.conditions
307 :conditions => s.conditions
307 ).group_by(&:assigned_to)
308 ).group_by(&:assigned_to)
308 issues_by_assignee.each do |assignee, issues|
309 issues_by_assignee.each do |assignee, issues|
309 deliver_reminder(assignee, issues, days) unless assignee.nil?
310 deliver_reminder(assignee, issues, days) unless assignee.nil?
310 end
311 end
311 end
312 end
312
313
313 private
314 private
314 def initialize_defaults(method_name)
315 def initialize_defaults(method_name)
315 super
316 super
317 @initial_language = current_language
316 set_language_if_valid Setting.default_language
318 set_language_if_valid Setting.default_language
317 from Setting.mail_from
319 from Setting.mail_from
318
320
319 # Common headers
321 # Common headers
320 headers 'X-Mailer' => 'Redmine',
322 headers 'X-Mailer' => 'Redmine',
321 'X-Redmine-Host' => Setting.host_name,
323 'X-Redmine-Host' => Setting.host_name,
322 'X-Redmine-Site' => Setting.app_title,
324 'X-Redmine-Site' => Setting.app_title,
323 'Precedence' => 'bulk',
325 'Precedence' => 'bulk',
324 'Auto-Submitted' => 'auto-generated'
326 'Auto-Submitted' => 'auto-generated'
325 end
327 end
326
328
327 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
329 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
328 def redmine_headers(h)
330 def redmine_headers(h)
329 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
331 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
330 end
332 end
331
333
332 # Overrides the create_mail method
334 # Overrides the create_mail method
333 def create_mail
335 def create_mail
334 # Removes the current user from the recipients and cc
336 # Removes the current user from the recipients and cc
335 # if he doesn't want to receive notifications about what he does
337 # if he doesn't want to receive notifications about what he does
336 @author ||= User.current
338 @author ||= User.current
337 if @author.pref[:no_self_notified]
339 if @author.pref[:no_self_notified]
338 recipients.delete(@author.mail) if recipients
340 recipients.delete(@author.mail) if recipients
339 cc.delete(@author.mail) if cc
341 cc.delete(@author.mail) if cc
340 end
342 end
341 # Blind carbon copy recipients
343 # Blind carbon copy recipients
342 if Setting.bcc_recipients?
344 if Setting.bcc_recipients?
343 bcc([recipients, cc].flatten.compact.uniq)
345 bcc([recipients, cc].flatten.compact.uniq)
344 recipients []
346 recipients []
345 cc []
347 cc []
346 end
348 end
347 super
349 super
348 end
350 end
349
351
350 # Rails 2.3 has problems rendering implicit multipart messages with
352 # Rails 2.3 has problems rendering implicit multipart messages with
351 # layouts so this method will wrap an multipart messages with
353 # layouts so this method will wrap an multipart messages with
352 # explicit parts.
354 # explicit parts.
353 #
355 #
354 # https://rails.lighthouseapp.com/projects/8994/tickets/2338-actionmailer-mailer-views-and-content-type
356 # https://rails.lighthouseapp.com/projects/8994/tickets/2338-actionmailer-mailer-views-and-content-type
355 # https://rails.lighthouseapp.com/projects/8994/tickets/1799-actionmailer-doesnt-set-template_format-when-rendering-layouts
357 # https://rails.lighthouseapp.com/projects/8994/tickets/1799-actionmailer-doesnt-set-template_format-when-rendering-layouts
356
358
357 def render_multipart(method_name, body)
359 def render_multipart(method_name, body)
358 if Setting.plain_text_mail?
360 if Setting.plain_text_mail?
359 content_type "text/plain"
361 content_type "text/plain"
360 body render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb')
362 body render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb')
361 else
363 else
362 content_type "multipart/alternative"
364 content_type "multipart/alternative"
363 part :content_type => "text/plain", :body => render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb')
365 part :content_type => "text/plain", :body => render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb')
364 part :content_type => "text/html", :body => render_message("#{method_name}.text.html.rhtml", body)
366 part :content_type => "text/html", :body => render_message("#{method_name}.text.html.rhtml", body)
365 end
367 end
366 end
368 end
367
369
368 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
370 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
369 def self.controller_path
371 def self.controller_path
370 ''
372 ''
371 end unless respond_to?('controller_path')
373 end unless respond_to?('controller_path')
372
374
373 # Returns a predictable Message-Id for the given object
375 # Returns a predictable Message-Id for the given object
374 def self.message_id_for(object)
376 def self.message_id_for(object)
375 # id + timestamp should reduce the odds of a collision
377 # id + timestamp should reduce the odds of a collision
376 # as far as we don't send multiple emails for the same object
378 # as far as we don't send multiple emails for the same object
377 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
379 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
378 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
380 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
379 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
381 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
380 host = "#{::Socket.gethostname}.redmine" if host.empty?
382 host = "#{::Socket.gethostname}.redmine" if host.empty?
381 "<#{hash}@#{host}>"
383 "<#{hash}@#{host}>"
382 end
384 end
383
385
384 private
386 private
385
387
386 def message_id(object)
388 def message_id(object)
387 @message_id_object = object
389 @message_id_object = object
388 end
390 end
389
391
390 def references(object)
392 def references(object)
391 @references_objects ||= []
393 @references_objects ||= []
392 @references_objects << object
394 @references_objects << object
393 end
395 end
394 end
396 end
395
397
396 # Patch TMail so that message_id is not overwritten
398 # Patch TMail so that message_id is not overwritten
397 module TMail
399 module TMail
398 class Mail
400 class Mail
399 def add_message_id( fqdn = nil )
401 def add_message_id( fqdn = nil )
400 self.message_id ||= ::TMail::new_message_id(fqdn)
402 self.message_id ||= ::TMail::new_message_id(fqdn)
401 end
403 end
402 end
404 end
403 end
405 end
@@ -1,299 +1,313
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class MailerTest < ActiveSupport::TestCase
20 class MailerTest < ActiveSupport::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22 include ActionController::Assertions::SelectorAssertions
22 include ActionController::Assertions::SelectorAssertions
23 fixtures :projects, :issues, :users, :members, :member_roles, :documents, :attachments, :news, :tokens, :journals, :journal_details, :changesets, :trackers, :issue_statuses, :enumerations, :messages, :boards, :repositories
23 fixtures :projects, :enabled_modules, :issues, :users, :members, :member_roles, :roles, :documents, :attachments, :news, :tokens, :journals, :journal_details, :changesets, :trackers, :issue_statuses, :enumerations, :messages, :boards, :repositories
24
24
25 def test_generated_links_in_emails
25 def test_generated_links_in_emails
26 ActionMailer::Base.deliveries.clear
26 ActionMailer::Base.deliveries.clear
27 Setting.host_name = 'mydomain.foo'
27 Setting.host_name = 'mydomain.foo'
28 Setting.protocol = 'https'
28 Setting.protocol = 'https'
29
29
30 journal = Journal.find(2)
30 journal = Journal.find(2)
31 assert Mailer.deliver_issue_edit(journal)
31 assert Mailer.deliver_issue_edit(journal)
32
32
33 mail = ActionMailer::Base.deliveries.last
33 mail = ActionMailer::Base.deliveries.last
34 assert_kind_of TMail::Mail, mail
34 assert_kind_of TMail::Mail, mail
35
35
36 assert_select_email do
36 assert_select_email do
37 # link to the main ticket
37 # link to the main ticket
38 assert_select "a[href=?]", "https://mydomain.foo/issues/1", :text => "Bug #1: Can't print recipes"
38 assert_select "a[href=?]", "https://mydomain.foo/issues/1", :text => "Bug #1: Can't print recipes"
39 # link to a referenced ticket
39 # link to a referenced ticket
40 assert_select "a[href=?][title=?]", "https://mydomain.foo/issues/2", "Add ingredients categories (Assigned)", :text => "#2"
40 assert_select "a[href=?][title=?]", "https://mydomain.foo/issues/2", "Add ingredients categories (Assigned)", :text => "#2"
41 # link to a changeset
41 # link to a changeset
42 assert_select "a[href=?][title=?]", "https://mydomain.foo/projects/ecookbook/repository/revisions/2", "This commit fixes #1, #2 and references #1 &amp; #3", :text => "r2"
42 assert_select "a[href=?][title=?]", "https://mydomain.foo/projects/ecookbook/repository/revisions/2", "This commit fixes #1, #2 and references #1 &amp; #3", :text => "r2"
43 end
43 end
44 end
44 end
45
45
46 def test_generated_links_with_prefix
46 def test_generated_links_with_prefix
47 relative_url_root = Redmine::Utils.relative_url_root
47 relative_url_root = Redmine::Utils.relative_url_root
48 ActionMailer::Base.deliveries.clear
48 ActionMailer::Base.deliveries.clear
49 Setting.host_name = 'mydomain.foo/rdm'
49 Setting.host_name = 'mydomain.foo/rdm'
50 Setting.protocol = 'http'
50 Setting.protocol = 'http'
51 Redmine::Utils.relative_url_root = '/rdm'
51 Redmine::Utils.relative_url_root = '/rdm'
52
52
53 journal = Journal.find(2)
53 journal = Journal.find(2)
54 assert Mailer.deliver_issue_edit(journal)
54 assert Mailer.deliver_issue_edit(journal)
55
55
56 mail = ActionMailer::Base.deliveries.last
56 mail = ActionMailer::Base.deliveries.last
57 assert_kind_of TMail::Mail, mail
57 assert_kind_of TMail::Mail, mail
58
58
59 assert_select_email do
59 assert_select_email do
60 # link to the main ticket
60 # link to the main ticket
61 assert_select "a[href=?]", "http://mydomain.foo/rdm/issues/1", :text => "Bug #1: Can't print recipes"
61 assert_select "a[href=?]", "http://mydomain.foo/rdm/issues/1", :text => "Bug #1: Can't print recipes"
62 # link to a referenced ticket
62 # link to a referenced ticket
63 assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/issues/2", "Add ingredients categories (Assigned)", :text => "#2"
63 assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/issues/2", "Add ingredients categories (Assigned)", :text => "#2"
64 # link to a changeset
64 # link to a changeset
65 assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2", "This commit fixes #1, #2 and references #1 &amp; #3", :text => "r2"
65 assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2", "This commit fixes #1, #2 and references #1 &amp; #3", :text => "r2"
66 end
66 end
67 ensure
67 ensure
68 # restore it
68 # restore it
69 Redmine::Utils.relative_url_root = relative_url_root
69 Redmine::Utils.relative_url_root = relative_url_root
70 end
70 end
71
71
72 def test_generated_links_with_prefix_and_no_relative_url_root
72 def test_generated_links_with_prefix_and_no_relative_url_root
73 relative_url_root = Redmine::Utils.relative_url_root
73 relative_url_root = Redmine::Utils.relative_url_root
74 ActionMailer::Base.deliveries.clear
74 ActionMailer::Base.deliveries.clear
75 Setting.host_name = 'mydomain.foo/rdm'
75 Setting.host_name = 'mydomain.foo/rdm'
76 Setting.protocol = 'http'
76 Setting.protocol = 'http'
77 Redmine::Utils.relative_url_root = nil
77 Redmine::Utils.relative_url_root = nil
78
78
79 journal = Journal.find(2)
79 journal = Journal.find(2)
80 assert Mailer.deliver_issue_edit(journal)
80 assert Mailer.deliver_issue_edit(journal)
81
81
82 mail = ActionMailer::Base.deliveries.last
82 mail = ActionMailer::Base.deliveries.last
83 assert_kind_of TMail::Mail, mail
83 assert_kind_of TMail::Mail, mail
84
84
85 assert_select_email do
85 assert_select_email do
86 # link to the main ticket
86 # link to the main ticket
87 assert_select "a[href=?]", "http://mydomain.foo/rdm/issues/1", :text => "Bug #1: Can't print recipes"
87 assert_select "a[href=?]", "http://mydomain.foo/rdm/issues/1", :text => "Bug #1: Can't print recipes"
88 # link to a referenced ticket
88 # link to a referenced ticket
89 assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/issues/2", "Add ingredients categories (Assigned)", :text => "#2"
89 assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/issues/2", "Add ingredients categories (Assigned)", :text => "#2"
90 # link to a changeset
90 # link to a changeset
91 assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2", "This commit fixes #1, #2 and references #1 &amp; #3", :text => "r2"
91 assert_select "a[href=?][title=?]", "http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2", "This commit fixes #1, #2 and references #1 &amp; #3", :text => "r2"
92 end
92 end
93 ensure
93 ensure
94 # restore it
94 # restore it
95 Redmine::Utils.relative_url_root = relative_url_root
95 Redmine::Utils.relative_url_root = relative_url_root
96 end
96 end
97
97
98 def test_email_headers
98 def test_email_headers
99 ActionMailer::Base.deliveries.clear
99 ActionMailer::Base.deliveries.clear
100 issue = Issue.find(1)
100 issue = Issue.find(1)
101 Mailer.deliver_issue_add(issue)
101 Mailer.deliver_issue_add(issue)
102 mail = ActionMailer::Base.deliveries.last
102 mail = ActionMailer::Base.deliveries.last
103 assert_not_nil mail
103 assert_not_nil mail
104 assert_equal 'bulk', mail.header_string('Precedence')
104 assert_equal 'bulk', mail.header_string('Precedence')
105 assert_equal 'auto-generated', mail.header_string('Auto-Submitted')
105 assert_equal 'auto-generated', mail.header_string('Auto-Submitted')
106 end
106 end
107
107
108 def test_plain_text_mail
108 def test_plain_text_mail
109 Setting.plain_text_mail = 1
109 Setting.plain_text_mail = 1
110 journal = Journal.find(2)
110 journal = Journal.find(2)
111 Mailer.deliver_issue_edit(journal)
111 Mailer.deliver_issue_edit(journal)
112 mail = ActionMailer::Base.deliveries.last
112 mail = ActionMailer::Base.deliveries.last
113 assert_equal "text/plain", mail.content_type
113 assert_equal "text/plain", mail.content_type
114 assert_equal 0, mail.parts.size
114 assert_equal 0, mail.parts.size
115 assert !mail.encoded.include?('href')
115 assert !mail.encoded.include?('href')
116 end
116 end
117
117
118 def test_html_mail
118 def test_html_mail
119 Setting.plain_text_mail = 0
119 Setting.plain_text_mail = 0
120 journal = Journal.find(2)
120 journal = Journal.find(2)
121 Mailer.deliver_issue_edit(journal)
121 Mailer.deliver_issue_edit(journal)
122 mail = ActionMailer::Base.deliveries.last
122 mail = ActionMailer::Base.deliveries.last
123 assert_equal 2, mail.parts.size
123 assert_equal 2, mail.parts.size
124 assert mail.encoded.include?('href')
124 assert mail.encoded.include?('href')
125 end
125 end
126
126
127 def test_issue_add_message_id
127 def test_issue_add_message_id
128 ActionMailer::Base.deliveries.clear
128 ActionMailer::Base.deliveries.clear
129 issue = Issue.find(1)
129 issue = Issue.find(1)
130 Mailer.deliver_issue_add(issue)
130 Mailer.deliver_issue_add(issue)
131 mail = ActionMailer::Base.deliveries.last
131 mail = ActionMailer::Base.deliveries.last
132 assert_not_nil mail
132 assert_not_nil mail
133 assert_equal Mailer.message_id_for(issue), mail.message_id
133 assert_equal Mailer.message_id_for(issue), mail.message_id
134 assert_nil mail.references
134 assert_nil mail.references
135 end
135 end
136
136
137 def test_issue_edit_message_id
137 def test_issue_edit_message_id
138 ActionMailer::Base.deliveries.clear
138 ActionMailer::Base.deliveries.clear
139 journal = Journal.find(1)
139 journal = Journal.find(1)
140 Mailer.deliver_issue_edit(journal)
140 Mailer.deliver_issue_edit(journal)
141 mail = ActionMailer::Base.deliveries.last
141 mail = ActionMailer::Base.deliveries.last
142 assert_not_nil mail
142 assert_not_nil mail
143 assert_equal Mailer.message_id_for(journal), mail.message_id
143 assert_equal Mailer.message_id_for(journal), mail.message_id
144 assert_equal Mailer.message_id_for(journal.issue), mail.references.first.to_s
144 assert_equal Mailer.message_id_for(journal.issue), mail.references.first.to_s
145 end
145 end
146
146
147 def test_message_posted_message_id
147 def test_message_posted_message_id
148 ActionMailer::Base.deliveries.clear
148 ActionMailer::Base.deliveries.clear
149 message = Message.find(1)
149 message = Message.find(1)
150 Mailer.deliver_message_posted(message)
150 Mailer.deliver_message_posted(message)
151 mail = ActionMailer::Base.deliveries.last
151 mail = ActionMailer::Base.deliveries.last
152 assert_not_nil mail
152 assert_not_nil mail
153 assert_equal Mailer.message_id_for(message), mail.message_id
153 assert_equal Mailer.message_id_for(message), mail.message_id
154 assert_nil mail.references
154 assert_nil mail.references
155 end
155 end
156
156
157 def test_reply_posted_message_id
157 def test_reply_posted_message_id
158 ActionMailer::Base.deliveries.clear
158 ActionMailer::Base.deliveries.clear
159 message = Message.find(3)
159 message = Message.find(3)
160 Mailer.deliver_message_posted(message)
160 Mailer.deliver_message_posted(message)
161 mail = ActionMailer::Base.deliveries.last
161 mail = ActionMailer::Base.deliveries.last
162 assert_not_nil mail
162 assert_not_nil mail
163 assert_equal Mailer.message_id_for(message), mail.message_id
163 assert_equal Mailer.message_id_for(message), mail.message_id
164 assert_equal Mailer.message_id_for(message.parent), mail.references.first.to_s
164 assert_equal Mailer.message_id_for(message.parent), mail.references.first.to_s
165 end
165 end
166
166
167 context("#issue_add") do
167 context("#issue_add") do
168 setup do
168 setup do
169 ActionMailer::Base.deliveries.clear
169 ActionMailer::Base.deliveries.clear
170 Setting.bcc_recipients = '1'
170 Setting.bcc_recipients = '1'
171 @issue = Issue.find(1)
171 @issue = Issue.find(1)
172 end
172 end
173
173
174 should "notify project members" do
174 should "notify project members" do
175 assert Mailer.deliver_issue_add(@issue)
175 assert Mailer.deliver_issue_add(@issue)
176 assert last_email.bcc.include?('dlopper@somenet.foo')
176 assert last_email.bcc.include?('dlopper@somenet.foo')
177 end
177 end
178
178
179 should "not notify project members that are not allow to view the issue" do
179 should "not notify project members that are not allow to view the issue" do
180 Role.find(2).remove_permission!(:view_issues)
180 Role.find(2).remove_permission!(:view_issues)
181 assert Mailer.deliver_issue_add(@issue)
181 assert Mailer.deliver_issue_add(@issue)
182 assert !last_email.bcc.include?('dlopper@somenet.foo')
182 assert !last_email.bcc.include?('dlopper@somenet.foo')
183 end
183 end
184
184
185 should "notify issue watchers" do
185 should "notify issue watchers" do
186 user = User.find(9)
186 user = User.find(9)
187 Watcher.create!(:watchable => @issue, :user => user)
187 Watcher.create!(:watchable => @issue, :user => user)
188 assert Mailer.deliver_issue_add(@issue)
188 assert Mailer.deliver_issue_add(@issue)
189 assert last_email.bcc.include?(user.mail)
189 assert last_email.bcc.include?(user.mail)
190 end
190 end
191
191
192 should "not notify watchers not allowed to view the issue" do
192 should "not notify watchers not allowed to view the issue" do
193 user = User.find(9)
193 user = User.find(9)
194 Watcher.create!(:watchable => @issue, :user => user)
194 Watcher.create!(:watchable => @issue, :user => user)
195 Role.non_member.remove_permission!(:view_issues)
195 Role.non_member.remove_permission!(:view_issues)
196 assert Mailer.deliver_issue_add(@issue)
196 assert Mailer.deliver_issue_add(@issue)
197 assert !last_email.bcc.include?(user.mail)
197 assert !last_email.bcc.include?(user.mail)
198 end
198 end
199 end
199 end
200
200
201 # test mailer methods for each language
201 # test mailer methods for each language
202 def test_issue_add
202 def test_issue_add
203 issue = Issue.find(1)
203 issue = Issue.find(1)
204 valid_languages.each do |lang|
204 valid_languages.each do |lang|
205 Setting.default_language = lang.to_s
205 Setting.default_language = lang.to_s
206 assert Mailer.deliver_issue_add(issue)
206 assert Mailer.deliver_issue_add(issue)
207 end
207 end
208 end
208 end
209
209
210 def test_issue_edit
210 def test_issue_edit
211 journal = Journal.find(1)
211 journal = Journal.find(1)
212 valid_languages.each do |lang|
212 valid_languages.each do |lang|
213 Setting.default_language = lang.to_s
213 Setting.default_language = lang.to_s
214 assert Mailer.deliver_issue_edit(journal)
214 assert Mailer.deliver_issue_edit(journal)
215 end
215 end
216 end
216 end
217
217
218 def test_document_added
218 def test_document_added
219 document = Document.find(1)
219 document = Document.find(1)
220 valid_languages.each do |lang|
220 valid_languages.each do |lang|
221 Setting.default_language = lang.to_s
221 Setting.default_language = lang.to_s
222 assert Mailer.deliver_document_added(document)
222 assert Mailer.deliver_document_added(document)
223 end
223 end
224 end
224 end
225
225
226 def test_attachments_added
226 def test_attachments_added
227 attachements = [ Attachment.find_by_container_type('Document') ]
227 attachements = [ Attachment.find_by_container_type('Document') ]
228 valid_languages.each do |lang|
228 valid_languages.each do |lang|
229 Setting.default_language = lang.to_s
229 Setting.default_language = lang.to_s
230 assert Mailer.deliver_attachments_added(attachements)
230 assert Mailer.deliver_attachments_added(attachements)
231 end
231 end
232 end
232 end
233
233
234 def test_news_added
234 def test_news_added
235 news = News.find(:first)
235 news = News.find(:first)
236 valid_languages.each do |lang|
236 valid_languages.each do |lang|
237 Setting.default_language = lang.to_s
237 Setting.default_language = lang.to_s
238 assert Mailer.deliver_news_added(news)
238 assert Mailer.deliver_news_added(news)
239 end
239 end
240 end
240 end
241
241
242 def test_message_posted
242 def test_message_posted
243 message = Message.find(:first)
243 message = Message.find(:first)
244 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
244 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
245 recipients = recipients.compact.uniq
245 recipients = recipients.compact.uniq
246 valid_languages.each do |lang|
246 valid_languages.each do |lang|
247 Setting.default_language = lang.to_s
247 Setting.default_language = lang.to_s
248 assert Mailer.deliver_message_posted(message)
248 assert Mailer.deliver_message_posted(message)
249 end
249 end
250 end
250 end
251
251
252 def test_account_information
252 def test_account_information
253 user = User.find(:first)
253 user = User.find(:first)
254 valid_languages.each do |lang|
254 valid_languages.each do |lang|
255 user.update_attribute :language, lang.to_s
255 user.update_attribute :language, lang.to_s
256 user.reload
256 user.reload
257 assert Mailer.deliver_account_information(user, 'pAsswORd')
257 assert Mailer.deliver_account_information(user, 'pAsswORd')
258 end
258 end
259 end
259 end
260
260
261 def test_lost_password
261 def test_lost_password
262 token = Token.find(2)
262 token = Token.find(2)
263 valid_languages.each do |lang|
263 valid_languages.each do |lang|
264 token.user.update_attribute :language, lang.to_s
264 token.user.update_attribute :language, lang.to_s
265 token.reload
265 token.reload
266 assert Mailer.deliver_lost_password(token)
266 assert Mailer.deliver_lost_password(token)
267 end
267 end
268 end
268 end
269
269
270 def test_register
270 def test_register
271 token = Token.find(1)
271 token = Token.find(1)
272 Setting.host_name = 'redmine.foo'
272 Setting.host_name = 'redmine.foo'
273 Setting.protocol = 'https'
273 Setting.protocol = 'https'
274
274
275 valid_languages.each do |lang|
275 valid_languages.each do |lang|
276 token.user.update_attribute :language, lang.to_s
276 token.user.update_attribute :language, lang.to_s
277 token.reload
277 token.reload
278 ActionMailer::Base.deliveries.clear
278 ActionMailer::Base.deliveries.clear
279 assert Mailer.deliver_register(token)
279 assert Mailer.deliver_register(token)
280 mail = ActionMailer::Base.deliveries.last
280 mail = ActionMailer::Base.deliveries.last
281 assert mail.body.include?("https://redmine.foo/account/activate?token=#{token.value}")
281 assert mail.body.include?("https://redmine.foo/account/activate?token=#{token.value}")
282 end
282 end
283 end
283 end
284
284
285 def test_reminders
285 def test_reminders
286 ActionMailer::Base.deliveries.clear
286 ActionMailer::Base.deliveries.clear
287 Mailer.reminders(:days => 42)
287 Mailer.reminders(:days => 42)
288 assert_equal 1, ActionMailer::Base.deliveries.size
288 assert_equal 1, ActionMailer::Base.deliveries.size
289 mail = ActionMailer::Base.deliveries.last
289 mail = ActionMailer::Base.deliveries.last
290 assert mail.bcc.include?('dlopper@somenet.foo')
290 assert mail.bcc.include?('dlopper@somenet.foo')
291 assert mail.body.include?('Bug #3: Error 281 when updating a recipe')
291 assert mail.body.include?('Bug #3: Error 281 when updating a recipe')
292 end
292 end
293
293
294 def last_email
294 def last_email
295 mail = ActionMailer::Base.deliveries.last
295 mail = ActionMailer::Base.deliveries.last
296 assert_not_nil mail
296 assert_not_nil mail
297 mail
297 mail
298 end
298 end
299
300 def test_mailer_should_not_change_locale
301 Setting.default_language = 'en'
302 # Set current language to italian
303 set_language_if_valid 'it'
304 # Send an email to a french user
305 user = User.find(1)
306 user.language = 'fr'
307 Mailer.deliver_account_activated(user)
308 mail = ActionMailer::Base.deliveries.last
309 assert mail.body.include?('Votre compte')
310
311 assert_equal :it, current_language
312 end
299 end
313 end
General Comments 0
You need to be logged in to leave comments. Login now