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