##// END OF EJS Templates
Added observers to watch model objects for mail delivery instead of calling Mailer....
Eric Davis -
r2548:b4be8849c0de
parent child
Show More
@@ -0,0 +1,22
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class DocumentObserver < ActiveRecord::Observer
19 def after_create(document)
20 Mailer.deliver_document_added(document) if Setting.notified_events.include?('document_added')
21 end
22 end
@@ -0,0 +1,22
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class IssueObserver < ActiveRecord::Observer
19 def after_create(issue)
20 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
21 end
22 end
@@ -0,0 +1,22
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class JournalObserver < ActiveRecord::Observer
19 def after_create(journal)
20 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
21 end
22 end
@@ -0,0 +1,22
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class NewsObserver < ActiveRecord::Observer
19 def after_create(news)
20 Mailer.deliver_news_added(news) if Setting.notified_events.include?('news_added')
21 end
22 end
@@ -1,88 +1,87
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 DocumentsController < ApplicationController
19 19 before_filter :find_project, :only => [:index, :new]
20 20 before_filter :find_document, :except => [:index, :new]
21 21 before_filter :authorize
22 22
23 23 helper :attachments
24 24
25 25 def index
26 26 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
27 27 documents = @project.documents.find :all, :include => [:attachments, :category]
28 28 case @sort_by
29 29 when 'date'
30 30 @grouped = documents.group_by {|d| d.created_on.to_date }
31 31 when 'title'
32 32 @grouped = documents.group_by {|d| d.title.first.upcase}
33 33 when 'author'
34 34 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
35 35 else
36 36 @grouped = documents.group_by(&:category)
37 37 end
38 38 @document = @project.documents.build
39 39 render :layout => false if request.xhr?
40 40 end
41 41
42 42 def show
43 43 @attachments = @document.attachments.find(:all, :order => "created_on DESC")
44 44 end
45 45
46 46 def new
47 47 @document = @project.documents.build(params[:document])
48 48 if request.post? and @document.save
49 49 attach_files(@document, params[:attachments])
50 50 flash[:notice] = l(:notice_successful_create)
51 Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
52 51 redirect_to :action => 'index', :project_id => @project
53 52 end
54 53 end
55 54
56 55 def edit
57 56 @categories = Enumeration.document_categories
58 57 if request.post? and @document.update_attributes(params[:document])
59 58 flash[:notice] = l(:notice_successful_update)
60 59 redirect_to :action => 'show', :id => @document
61 60 end
62 61 end
63 62
64 63 def destroy
65 64 @document.destroy
66 65 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
67 66 end
68 67
69 68 def add_attachment
70 69 attachments = attach_files(@document, params[:attachments])
71 70 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
72 71 redirect_to :action => 'show', :id => @document
73 72 end
74 73
75 74 private
76 75 def find_project
77 76 @project = Project.find(params[:project_id])
78 77 rescue ActiveRecord::RecordNotFound
79 78 render_404
80 79 end
81 80
82 81 def find_document
83 82 @document = Document.find(params[:id])
84 83 @project = @document.project
85 84 rescue ActiveRecord::RecordNotFound
86 85 render_404
87 86 end
88 87 end
@@ -1,497 +1,492
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
21 21 before_filter :find_issue, :only => [:show, :edit, :reply]
22 22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 26 accept_key_auth :index, :changes
27 27
28 28 helper :journals
29 29 helper :projects
30 30 include ProjectsHelper
31 31 helper :custom_fields
32 32 include CustomFieldsHelper
33 33 helper :issue_relations
34 34 include IssueRelationsHelper
35 35 helper :watchers
36 36 include WatchersHelper
37 37 helper :attachments
38 38 include AttachmentsHelper
39 39 helper :queries
40 40 helper :sort
41 41 include SortHelper
42 42 include IssuesHelper
43 43 helper :timelog
44 44 include Redmine::Export::PDF
45 45
46 46 def index
47 47 retrieve_query
48 48 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
49 49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
50 50
51 51 if @query.valid?
52 52 limit = per_page_option
53 53 respond_to do |format|
54 54 format.html { }
55 55 format.atom { }
56 56 format.csv { limit = Setting.issues_export_limit.to_i }
57 57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 58 end
59 59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 61 @issues = Issue.find :all, :order => sort_clause,
62 62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 63 :conditions => @query.statement,
64 64 :limit => limit,
65 65 :offset => @issue_pages.current.offset
66 66 respond_to do |format|
67 67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 70 format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
71 71 end
72 72 else
73 73 # Send html if the query is not valid
74 74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 75 end
76 76 rescue ActiveRecord::RecordNotFound
77 77 render_404
78 78 end
79 79
80 80 def changes
81 81 retrieve_query
82 82 sort_init 'id', 'desc'
83 83 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
84 84
85 85 if @query.valid?
86 86 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
87 87 :conditions => @query.statement,
88 88 :limit => 25,
89 89 :order => "#{Journal.table_name}.created_on DESC"
90 90 end
91 91 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
92 92 render :layout => false, :content_type => 'application/atom+xml'
93 93 rescue ActiveRecord::RecordNotFound
94 94 render_404
95 95 end
96 96
97 97 def show
98 98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
99 99 @journals.each_with_index {|j,i| j.indice = i+1}
100 100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
101 101 @changesets = @issue.changesets
102 102 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
103 103 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
104 104 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
105 105 @priorities = Enumeration.priorities
106 106 @time_entry = TimeEntry.new
107 107 respond_to do |format|
108 108 format.html { render :template => 'issues/show.rhtml' }
109 109 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
110 110 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
111 111 end
112 112 end
113 113
114 114 # Add a new issue
115 115 # The new issue will be created from an existing one if copy_from parameter is given
116 116 def new
117 117 @issue = Issue.new
118 118 @issue.copy_from(params[:copy_from]) if params[:copy_from]
119 119 @issue.project = @project
120 120 # Tracker must be set before custom field values
121 121 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
122 122 if @issue.tracker.nil?
123 123 render_error 'No tracker is associated to this project. Please check the Project settings.'
124 124 return
125 125 end
126 126 if params[:issue].is_a?(Hash)
127 127 @issue.attributes = params[:issue]
128 128 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
129 129 end
130 130 @issue.author = User.current
131 131
132 132 default_status = IssueStatus.default
133 133 unless default_status
134 134 render_error 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
135 135 return
136 136 end
137 137 @issue.status = default_status
138 138 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
139 139
140 140 if request.get? || request.xhr?
141 141 @issue.start_date ||= Date.today
142 142 else
143 143 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
144 144 # Check that the user is allowed to apply the requested status
145 145 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
146 146 if @issue.save
147 147 attach_files(@issue, params[:attachments])
148 148 flash[:notice] = l(:notice_successful_create)
149 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
150 149 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
151 150 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
152 151 { :action => 'show', :id => @issue })
153 152 return
154 153 end
155 154 end
156 155 @priorities = Enumeration.priorities
157 156 render :layout => !request.xhr?
158 157 end
159 158
160 159 # Attributes that can be updated on workflow transition (without :edit permission)
161 160 # TODO: make it configurable (at least per role)
162 161 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
163 162
164 163 def edit
165 164 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
166 165 @priorities = Enumeration.priorities
167 166 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
168 167 @time_entry = TimeEntry.new
169 168
170 169 @notes = params[:notes]
171 170 journal = @issue.init_journal(User.current, @notes)
172 171 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
173 172 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
174 173 attrs = params[:issue].dup
175 174 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
176 175 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
177 176 @issue.attributes = attrs
178 177 end
179 178
180 179 if request.post?
181 180 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
182 181 @time_entry.attributes = params[:time_entry]
183 182 attachments = attach_files(@issue, params[:attachments])
184 183 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
185 184
186 185 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
187 186
188 187 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
189 188 # Log spend time
190 189 if User.current.allowed_to?(:log_time, @project)
191 190 @time_entry.save
192 191 end
193 192 if !journal.new_record?
194 193 # Only send notification if something was actually changed
195 194 flash[:notice] = l(:notice_successful_update)
196 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
197 195 end
198 196 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
199 197 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
200 198 end
201 199 end
202 200 rescue ActiveRecord::StaleObjectError
203 201 # Optimistic locking exception
204 202 flash.now[:error] = l(:notice_locking_conflict)
205 203 end
206 204
207 205 def reply
208 206 journal = Journal.find(params[:journal_id]) if params[:journal_id]
209 207 if journal
210 208 user = journal.user
211 209 text = journal.notes
212 210 else
213 211 user = @issue.author
214 212 text = @issue.description
215 213 end
216 214 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
217 215 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
218 216 render(:update) { |page|
219 217 page.<< "$('notes').value = \"#{content}\";"
220 218 page.show 'update'
221 219 page << "Form.Element.focus('notes');"
222 220 page << "Element.scrollTo('update');"
223 221 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
224 222 }
225 223 end
226 224
227 225 # Bulk edit a set of issues
228 226 def bulk_edit
229 227 if request.post?
230 228 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
231 229 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
232 230 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
233 231 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
234 232 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
235 233 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
236 234
237 235 unsaved_issue_ids = []
238 236 @issues.each do |issue|
239 237 journal = issue.init_journal(User.current, params[:notes])
240 238 issue.priority = priority if priority
241 239 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
242 240 issue.category = category if category || params[:category_id] == 'none'
243 241 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
244 242 issue.start_date = params[:start_date] unless params[:start_date].blank?
245 243 issue.due_date = params[:due_date] unless params[:due_date].blank?
246 244 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
247 245 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
248 246 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
249 247 # Don't save any change to the issue if the user is not authorized to apply the requested status
250 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
251 # Send notification for each issue (if changed)
252 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
253 else
248 unless (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
254 249 # Keep unsaved issue ids to display them in flash error
255 250 unsaved_issue_ids << issue.id
256 251 end
257 252 end
258 253 if unsaved_issue_ids.empty?
259 254 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
260 255 else
261 256 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
262 257 :total => @issues.size,
263 258 :ids => '#' + unsaved_issue_ids.join(', #'))
264 259 end
265 260 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
266 261 return
267 262 end
268 263 # Find potential statuses the user could be allowed to switch issues to
269 264 @available_statuses = Workflow.find(:all, :include => :new_status,
270 265 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
271 266 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
272 267 end
273 268
274 269 def move
275 270 @allowed_projects = []
276 271 # find projects to which the user is allowed to move the issue
277 272 if User.current.admin?
278 273 # admin is allowed to move issues to any active (visible) project
279 274 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
280 275 else
281 276 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
282 277 end
283 278 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
284 279 @target_project ||= @project
285 280 @trackers = @target_project.trackers
286 281 if request.post?
287 282 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
288 283 unsaved_issue_ids = []
289 284 @issues.each do |issue|
290 285 issue.init_journal(User.current)
291 286 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
292 287 end
293 288 if unsaved_issue_ids.empty?
294 289 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
295 290 else
296 291 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
297 292 :total => @issues.size,
298 293 :ids => '#' + unsaved_issue_ids.join(', #'))
299 294 end
300 295 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
301 296 return
302 297 end
303 298 render :layout => false if request.xhr?
304 299 end
305 300
306 301 def destroy
307 302 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
308 303 if @hours > 0
309 304 case params[:todo]
310 305 when 'destroy'
311 306 # nothing to do
312 307 when 'nullify'
313 308 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
314 309 when 'reassign'
315 310 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
316 311 if reassign_to.nil?
317 312 flash.now[:error] = l(:error_issue_not_found_in_project)
318 313 return
319 314 else
320 315 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
321 316 end
322 317 else
323 318 # display the destroy form
324 319 return
325 320 end
326 321 end
327 322 @issues.each(&:destroy)
328 323 redirect_to :action => 'index', :project_id => @project
329 324 end
330 325
331 326 def gantt
332 327 @gantt = Redmine::Helpers::Gantt.new(params)
333 328 retrieve_query
334 329 if @query.valid?
335 330 events = []
336 331 # Issues that have start and due dates
337 332 events += Issue.find(:all,
338 333 :order => "start_date, due_date",
339 334 :include => [:tracker, :status, :assigned_to, :priority, :project],
340 335 :conditions => ["(#{@query.statement}) AND (((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]
341 336 )
342 337 # Issues that don't have a due date but that are assigned to a version with a date
343 338 events += Issue.find(:all,
344 339 :order => "start_date, effective_date",
345 340 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
346 341 :conditions => ["(#{@query.statement}) AND (((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]
347 342 )
348 343 # Versions
349 344 events += Version.find(:all, :include => :project,
350 345 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
351 346
352 347 @gantt.events = events
353 348 end
354 349
355 350 respond_to do |format|
356 351 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
357 352 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.png") } if @gantt.respond_to?('to_image')
358 353 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
359 354 end
360 355 end
361 356
362 357 def calendar
363 358 if params[:year] and params[:year].to_i > 1900
364 359 @year = params[:year].to_i
365 360 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
366 361 @month = params[:month].to_i
367 362 end
368 363 end
369 364 @year ||= Date.today.year
370 365 @month ||= Date.today.month
371 366
372 367 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
373 368 retrieve_query
374 369 if @query.valid?
375 370 events = []
376 371 events += Issue.find(:all,
377 372 :include => [:tracker, :status, :assigned_to, :priority, :project],
378 373 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
379 374 )
380 375 events += Version.find(:all, :include => :project,
381 376 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
382 377
383 378 @calendar.events = events
384 379 end
385 380
386 381 render :layout => false if request.xhr?
387 382 end
388 383
389 384 def context_menu
390 385 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
391 386 if (@issues.size == 1)
392 387 @issue = @issues.first
393 388 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
394 389 end
395 390 projects = @issues.collect(&:project).compact.uniq
396 391 @project = projects.first if projects.size == 1
397 392
398 393 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
399 394 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
400 395 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
401 396 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
402 397 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
403 398 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
404 399 }
405 400 if @project
406 401 @assignables = @project.assignable_users
407 402 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
408 403 end
409 404
410 405 @priorities = Enumeration.priorities.reverse
411 406 @statuses = IssueStatus.find(:all, :order => 'position')
412 407 @back = request.env['HTTP_REFERER']
413 408
414 409 render :layout => false
415 410 end
416 411
417 412 def update_form
418 413 @issue = Issue.new(params[:issue])
419 414 render :action => :new, :layout => false
420 415 end
421 416
422 417 def preview
423 418 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
424 419 @attachements = @issue.attachments if @issue
425 420 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
426 421 render :partial => 'common/preview'
427 422 end
428 423
429 424 private
430 425 def find_issue
431 426 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
432 427 @project = @issue.project
433 428 rescue ActiveRecord::RecordNotFound
434 429 render_404
435 430 end
436 431
437 432 # Filter for bulk operations
438 433 def find_issues
439 434 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
440 435 raise ActiveRecord::RecordNotFound if @issues.empty?
441 436 projects = @issues.collect(&:project).compact.uniq
442 437 if projects.size == 1
443 438 @project = projects.first
444 439 else
445 440 # TODO: let users bulk edit/move/destroy issues from different projects
446 441 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
447 442 end
448 443 rescue ActiveRecord::RecordNotFound
449 444 render_404
450 445 end
451 446
452 447 def find_project
453 448 @project = Project.find(params[:project_id])
454 449 rescue ActiveRecord::RecordNotFound
455 450 render_404
456 451 end
457 452
458 453 def find_optional_project
459 454 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
460 455 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
461 456 allowed ? true : deny_access
462 457 rescue ActiveRecord::RecordNotFound
463 458 render_404
464 459 end
465 460
466 461 # Retrieve query from session or build a new query
467 462 def retrieve_query
468 463 if !params[:query_id].blank?
469 464 cond = "project_id IS NULL"
470 465 cond << " OR project_id = #{@project.id}" if @project
471 466 @query = Query.find(params[:query_id], :conditions => cond)
472 467 @query.project = @project
473 468 session[:query] = {:id => @query.id, :project_id => @query.project_id}
474 469 sort_clear
475 470 else
476 471 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
477 472 # Give it a name, required to be valid
478 473 @query = Query.new(:name => "_")
479 474 @query.project = @project
480 475 if params[:fields] and params[:fields].is_a? Array
481 476 params[:fields].each do |field|
482 477 @query.add_filter(field, params[:operators][field], params[:values][field])
483 478 end
484 479 else
485 480 @query.available_filters.keys.each do |field|
486 481 @query.add_short_filter(field, params[field]) if params[field]
487 482 end
488 483 end
489 484 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
490 485 else
491 486 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
492 487 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
493 488 @query.project = @project
494 489 end
495 490 end
496 491 end
497 492 end
@@ -1,108 +1,107
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 NewsController < ApplicationController
19 19 before_filter :find_news, :except => [:new, :index, :preview]
20 20 before_filter :find_project, :only => [:new, :preview]
21 21 before_filter :authorize, :except => [:index, :preview]
22 22 before_filter :find_optional_project, :only => :index
23 23 accept_key_auth :index
24 24
25 25 def index
26 26 @news_pages, @newss = paginate :news,
27 27 :per_page => 10,
28 28 :conditions => (@project ? {:project_id => @project.id} : Project.visible_by(User.current)),
29 29 :include => [:author, :project],
30 30 :order => "#{News.table_name}.created_on DESC"
31 31 respond_to do |format|
32 32 format.html { render :layout => false if request.xhr? }
33 33 format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
34 34 end
35 35 end
36 36
37 37 def show
38 38 @comments = @news.comments
39 39 @comments.reverse! if User.current.wants_comments_in_reverse_order?
40 40 end
41 41
42 42 def new
43 43 @news = News.new(:project => @project, :author => User.current)
44 44 if request.post?
45 45 @news.attributes = params[:news]
46 46 if @news.save
47 47 flash[:notice] = l(:notice_successful_create)
48 Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
49 48 redirect_to :controller => 'news', :action => 'index', :project_id => @project
50 49 end
51 50 end
52 51 end
53 52
54 53 def edit
55 54 if request.post? and @news.update_attributes(params[:news])
56 55 flash[:notice] = l(:notice_successful_update)
57 56 redirect_to :action => 'show', :id => @news
58 57 end
59 58 end
60 59
61 60 def add_comment
62 61 @comment = Comment.new(params[:comment])
63 62 @comment.author = User.current
64 63 if @news.comments << @comment
65 64 flash[:notice] = l(:label_comment_added)
66 65 redirect_to :action => 'show', :id => @news
67 66 else
68 67 render :action => 'show'
69 68 end
70 69 end
71 70
72 71 def destroy_comment
73 72 @news.comments.find(params[:comment_id]).destroy
74 73 redirect_to :action => 'show', :id => @news
75 74 end
76 75
77 76 def destroy
78 77 @news.destroy
79 78 redirect_to :action => 'index', :project_id => @project
80 79 end
81 80
82 81 def preview
83 82 @text = (params[:news] ? params[:news][:description] : nil)
84 83 render :partial => 'common/preview'
85 84 end
86 85
87 86 private
88 87 def find_news
89 88 @news = News.find(params[:id])
90 89 @project = @news.project
91 90 rescue ActiveRecord::RecordNotFound
92 91 render_404
93 92 end
94 93
95 94 def find_project
96 95 @project = Project.find(params[:project_id])
97 96 rescue ActiveRecord::RecordNotFound
98 97 render_404
99 98 end
100 99
101 100 def find_optional_project
102 101 return true unless params[:project_id]
103 102 @project = Project.find(params[:project_id])
104 103 authorize
105 104 rescue ActiveRecord::RecordNotFound
106 105 render_404
107 106 end
108 107 end
@@ -1,169 +1,168
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 require 'iconv'
19 19
20 20 class Changeset < ActiveRecord::Base
21 21 belongs_to :repository
22 22 belongs_to :user
23 23 has_many :changes, :dependent => :delete_all
24 24 has_and_belongs_to_many :issues
25 25
26 26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
27 27 :description => :long_comments,
28 28 :datetime => :committed_on,
29 29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
30 30
31 31 acts_as_searchable :columns => 'comments',
32 32 :include => {:repository => :project},
33 33 :project_key => "#{Repository.table_name}.project_id",
34 34 :date_column => 'committed_on'
35 35
36 36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
37 37 :author_key => :user_id,
38 38 :find_options => {:include => {:repository => :project}}
39 39
40 40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
41 41 validates_uniqueness_of :revision, :scope => :repository_id
42 42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
43 43
44 44 def revision=(r)
45 45 write_attribute :revision, (r.nil? ? nil : r.to_s)
46 46 end
47 47
48 48 def comments=(comment)
49 49 write_attribute(:comments, Changeset.normalize_comments(comment))
50 50 end
51 51
52 52 def committed_on=(date)
53 53 self.commit_date = date
54 54 super
55 55 end
56 56
57 57 def project
58 58 repository.project
59 59 end
60 60
61 61 def author
62 62 user || committer.to_s.split('<').first
63 63 end
64 64
65 65 def before_create
66 66 self.user = repository.find_committer_user(committer)
67 67 end
68 68
69 69 def after_create
70 70 scan_comment_for_issue_ids
71 71 end
72 72 require 'pp'
73 73
74 74 def scan_comment_for_issue_ids
75 75 return if comments.blank?
76 76 # keywords used to reference issues
77 77 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
78 78 # keywords used to fix issues
79 79 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
80 80 # status and optional done ratio applied
81 81 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
82 82 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
83 83
84 84 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
85 85 return if kw_regexp.blank?
86 86
87 87 referenced_issues = []
88 88
89 89 if ref_keywords.delete('*')
90 90 # find any issue ID in the comments
91 91 target_issue_ids = []
92 92 comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
93 93 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
94 94 end
95 95
96 96 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
97 97 action = match[0]
98 98 target_issue_ids = match[1].scan(/\d+/)
99 99 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
100 100 if fix_status && fix_keywords.include?(action.downcase)
101 101 # update status of issues
102 102 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
103 103 target_issues.each do |issue|
104 104 # the issue may have been updated by the closure of another one (eg. duplicate)
105 105 issue.reload
106 106 # don't change the status is the issue is closed
107 107 next if issue.status.is_closed?
108 108 csettext = "r#{self.revision}"
109 109 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
110 110 csettext = "commit:\"#{self.scmid}\""
111 111 end
112 112 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
113 113 issue.status = fix_status
114 114 issue.done_ratio = done_ratio if done_ratio
115 115 issue.save
116 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
117 116 end
118 117 end
119 118 referenced_issues += target_issues
120 119 end
121 120
122 121 self.issues = referenced_issues.uniq
123 122 end
124 123
125 124 def short_comments
126 125 @short_comments || split_comments.first
127 126 end
128 127
129 128 def long_comments
130 129 @long_comments || split_comments.last
131 130 end
132 131
133 132 # Returns the previous changeset
134 133 def previous
135 134 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
136 135 end
137 136
138 137 # Returns the next changeset
139 138 def next
140 139 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
141 140 end
142 141
143 142 # Strips and reencodes a commit log before insertion into the database
144 143 def self.normalize_comments(str)
145 144 to_utf8(str.to_s.strip)
146 145 end
147 146
148 147 private
149 148
150 149 def split_comments
151 150 comments =~ /\A(.+?)\r?\n(.*)$/m
152 151 @short_comments = $1 || comments
153 152 @long_comments = $2.to_s.strip
154 153 return @short_comments, @long_comments
155 154 end
156 155
157 156 def self.to_utf8(str)
158 157 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
159 158 encoding = Setting.commit_logs_encoding.to_s.strip
160 159 unless encoding.blank? || encoding == 'UTF-8'
161 160 begin
162 161 return Iconv.conv('UTF-8', encoding, str)
163 162 rescue Iconv::Failure
164 163 # do nothing here
165 164 end
166 165 end
167 166 str
168 167 end
169 168 end
@@ -1,245 +1,242
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 MailHandler < ActionMailer::Base
19 19 include ActionView::Helpers::SanitizeHelper
20 20
21 21 class UnauthorizedAction < StandardError; end
22 22 class MissingInformation < StandardError; end
23 23
24 24 attr_reader :email, :user
25 25
26 26 def self.receive(email, options={})
27 27 @@handler_options = options.dup
28 28
29 29 @@handler_options[:issue] ||= {}
30 30
31 31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 32 @@handler_options[:allow_override] ||= []
33 33 # Project needs to be overridable if not specified
34 34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 35 # Status overridable by default
36 36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37 37 super email
38 38 end
39 39
40 40 # Processes incoming emails
41 41 def receive(email)
42 42 @email = email
43 43 @user = User.active.find_by_mail(email.from.to_a.first.to_s.strip)
44 44 unless @user
45 45 # Unknown user => the email is ignored
46 46 # TODO: ability to create the user's account
47 47 logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
48 48 return false
49 49 end
50 50 User.current = @user
51 51 dispatch
52 52 end
53 53
54 54 private
55 55
56 56 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
57 57 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
58 58 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]+msg(\d+)\]}
59 59
60 60 def dispatch
61 61 headers = [email.in_reply_to, email.references].flatten.compact
62 62 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
63 63 klass, object_id = $1, $2.to_i
64 64 method_name = "receive_#{klass}_reply"
65 65 if self.class.private_instance_methods.include?(method_name)
66 66 send method_name, object_id
67 67 else
68 68 # ignoring it
69 69 end
70 70 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
71 71 receive_issue_reply(m[1].to_i)
72 72 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
73 73 receive_message_reply(m[1].to_i)
74 74 else
75 75 receive_issue
76 76 end
77 77 rescue ActiveRecord::RecordInvalid => e
78 78 # TODO: send a email to the user
79 79 logger.error e.message if logger
80 80 false
81 81 rescue MissingInformation => e
82 82 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
83 83 false
84 84 rescue UnauthorizedAction => e
85 85 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
86 86 false
87 87 end
88 88
89 89 # Creates a new issue
90 90 def receive_issue
91 91 project = target_project
92 92 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
93 93 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
94 94 priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
95 95 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
96 96
97 97 # check permission
98 98 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
99 99 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
100 100 # check workflow
101 101 if status && issue.new_statuses_allowed_to(user).include?(status)
102 102 issue.status = status
103 103 end
104 104 issue.subject = email.subject.chomp.toutf8
105 105 issue.description = plain_text_body
106 106 # custom fields
107 107 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
108 108 if value = get_keyword(c.name, :override => true)
109 109 h[c.id] = value
110 110 end
111 111 h
112 112 end
113 # add To and Cc as watchers before saving so the watchers can reply to Redmine
114 add_watchers(issue)
113 115 issue.save!
114 116 add_attachments(issue)
115 117 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
116 # add To and Cc as watchers
117 add_watchers(issue)
118 # send notification after adding watchers so that they can reply to Redmine
119 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
120 118 issue
121 119 end
122 120
123 121 def target_project
124 122 # TODO: other ways to specify project:
125 123 # * parse the email To field
126 124 # * specific project (eg. Setting.mail_handler_target_project)
127 125 target = Project.find_by_identifier(get_keyword(:project))
128 126 raise MissingInformation.new('Unable to determine target project') if target.nil?
129 127 target
130 128 end
131 129
132 130 # Adds a note to an existing issue
133 131 def receive_issue_reply(issue_id)
134 132 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
135 133
136 134 issue = Issue.find_by_id(issue_id)
137 135 return unless issue
138 136 # check permission
139 137 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
140 138 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
141 139
142 140 # add the note
143 141 journal = issue.init_journal(user, plain_text_body)
144 142 add_attachments(issue)
145 143 # check workflow
146 144 if status && issue.new_statuses_allowed_to(user).include?(status)
147 145 issue.status = status
148 146 end
149 147 issue.save!
150 148 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
151 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
152 149 journal
153 150 end
154 151
155 152 # Reply will be added to the issue
156 153 def receive_journal_reply(journal_id)
157 154 journal = Journal.find_by_id(journal_id)
158 155 if journal && journal.journalized_type == 'Issue'
159 156 receive_issue_reply(journal.journalized_id)
160 157 end
161 158 end
162 159
163 160 # Receives a reply to a forum message
164 161 def receive_message_reply(message_id)
165 162 message = Message.find_by_id(message_id)
166 163 if message
167 164 message = message.root
168 165 if user.allowed_to?(:add_messages, message.project) && !message.locked?
169 166 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
170 167 :content => plain_text_body)
171 168 reply.author = user
172 169 reply.board = message.board
173 170 message.children << reply
174 171 add_attachments(reply)
175 172 reply
176 173 else
177 174 raise UnauthorizedAction
178 175 end
179 176 end
180 177 end
181 178
182 179 def add_attachments(obj)
183 180 if email.has_attachments?
184 181 email.attachments.each do |attachment|
185 182 Attachment.create(:container => obj,
186 183 :file => attachment,
187 184 :author => user,
188 185 :content_type => attachment.content_type)
189 186 end
190 187 end
191 188 end
192 189
193 190 # Adds To and Cc as watchers of the given object if the sender has the
194 191 # appropriate permission
195 192 def add_watchers(obj)
196 193 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
197 194 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
198 195 unless addresses.empty?
199 196 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
200 197 watchers.each {|w| obj.add_watcher(w)}
201 198 end
202 199 end
203 200 end
204 201
205 202 def get_keyword(attr, options={})
206 203 @keywords ||= {}
207 204 if @keywords.has_key?(attr)
208 205 @keywords[attr]
209 206 else
210 207 @keywords[attr] = begin
211 208 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}:[ \t]*(.+)\s*$/i, '')
212 209 $1.strip
213 210 elsif !@@handler_options[:issue][attr].blank?
214 211 @@handler_options[:issue][attr]
215 212 end
216 213 end
217 214 end
218 215 end
219 216
220 217 # Returns the text/plain part of the email
221 218 # If not found (eg. HTML-only email), returns the body with tags removed
222 219 def plain_text_body
223 220 return @plain_text_body unless @plain_text_body.nil?
224 221 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
225 222 if parts.empty?
226 223 parts << @email
227 224 end
228 225 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
229 226 if plain_text_part.nil?
230 227 # no text/plain part found, assuming html-only email
231 228 # strip html tags and remove doctype directive
232 229 @plain_text_body = strip_tags(@email.body.to_s)
233 230 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
234 231 else
235 232 @plain_text_body = plain_text_part.body.to_s
236 233 end
237 234 @plain_text_body.strip!
238 235 @plain_text_body
239 236 end
240 237
241 238
242 239 def self.full_sanitizer
243 240 @full_sanitizer ||= HTML::FullSanitizer.new
244 241 end
245 242 end
@@ -1,52 +1,52
1 1 # Be sure to restart your web server when you modify this file.
2 2
3 3 # Uncomment below to force Rails into production mode when
4 4 # you don't control web/app server and can't set it the proper way
5 5 # ENV['RAILS_ENV'] ||= 'production'
6 6
7 7 # Specifies gem version of Rails to use when vendor/rails is not present
8 8 RAILS_GEM_VERSION = '2.2.2' unless defined? RAILS_GEM_VERSION
9 9
10 10 # Bootstrap the Rails environment, frameworks, and default configuration
11 11 require File.join(File.dirname(__FILE__), 'boot')
12 12
13 13 # Load Engine plugin if available
14 14 begin
15 15 require File.join(File.dirname(__FILE__), '../vendor/plugins/engines/boot')
16 16 rescue LoadError
17 17 # Not available
18 18 end
19 19
20 20 Rails::Initializer.run do |config|
21 21 # Settings in config/environments/* take precedence those specified here
22 22
23 23 # Skip frameworks you're not going to use
24 24 # config.frameworks -= [ :action_web_service, :action_mailer ]
25 25
26 26 # Add additional load paths for sweepers
27 27 config.load_paths += %W( #{RAILS_ROOT}/app/sweepers )
28 28
29 29 # Force all environments to use the same logger level
30 30 # (by default production uses :info, the others :debug)
31 31 # config.log_level = :debug
32 32
33 33 # Enable page/fragment caching by setting a file-based store
34 34 # (remember to create the caching directory and make it readable to the application)
35 35 # config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"
36 36
37 37 # Activate observers that should always be running
38 38 # config.active_record.observers = :cacher, :garbage_collector
39 config.active_record.observers = :message_observer
39 config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer
40 40
41 41 # Make Active Record use UTC-base instead of local time
42 42 # config.active_record.default_timezone = :utc
43 43
44 44 # Use Active Record's schema dumper instead of SQL when creating the test database
45 45 # (enables use of different database adapters for development and test environments)
46 46 # config.active_record.schema_format = :ruby
47 47
48 48 # Deliveries are disabled by default. Do NOT modify this section.
49 49 # Define your email configuration in email.yml instead.
50 50 # It will automatically turn deliveries on
51 51 config.action_mailer.perform_deliveries = false
52 52 end
@@ -1,118 +1,121
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 require 'documents_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class DocumentsController; def rescue_action(e) raise e end; end
23 23
24 24 class DocumentsControllerTest < Test::Unit::TestCase
25 25 fixtures :projects, :users, :roles, :members, :enabled_modules, :documents, :enumerations
26 26
27 27 def setup
28 28 @controller = DocumentsController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_index_routing
35 35 assert_routing(
36 36 {:method => :get, :path => '/projects/567/documents'},
37 37 :controller => 'documents', :action => 'index', :project_id => '567'
38 38 )
39 39 end
40 40
41 41 def test_index
42 42 # Sets a default category
43 43 e = Enumeration.find_by_name('Technical documentation')
44 44 e.update_attributes(:is_default => true)
45 45
46 46 get :index, :project_id => 'ecookbook'
47 47 assert_response :success
48 48 assert_template 'index'
49 49 assert_not_nil assigns(:grouped)
50 50
51 51 # Default category selected in the new document form
52 52 assert_tag :select, :attributes => {:name => 'document[category_id]'},
53 53 :child => {:tag => 'option', :attributes => {:selected => 'selected'},
54 54 :content => 'Technical documentation'}
55 55 end
56 56
57 57 def test_new_routing
58 58 assert_routing(
59 59 {:method => :get, :path => '/projects/567/documents/new'},
60 60 :controller => 'documents', :action => 'new', :project_id => '567'
61 61 )
62 62 assert_recognizes(
63 63 {:controller => 'documents', :action => 'new', :project_id => '567'},
64 64 {:method => :post, :path => '/projects/567/documents'}
65 65 )
66 66 end
67 67
68 68 def test_new_with_one_attachment
69 ActionMailer::Base.deliveries.clear
70 Setting.notified_events << 'document_added'
69 71 @request.session[:user_id] = 2
70 72 set_tmp_attachments_directory
71 73
72 74 post :new, :project_id => 'ecookbook',
73 75 :document => { :title => 'DocumentsControllerTest#test_post_new',
74 76 :description => 'This is a new document',
75 77 :category_id => 2},
76 78 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
77 79
78 80 assert_redirected_to 'projects/ecookbook/documents'
79 81
80 82 document = Document.find_by_title('DocumentsControllerTest#test_post_new')
81 83 assert_not_nil document
82 84 assert_equal Enumeration.find(2), document.category
83 85 assert_equal 1, document.attachments.size
84 86 assert_equal 'testfile.txt', document.attachments.first.filename
87 assert_equal 1, ActionMailer::Base.deliveries.size
85 88 end
86 89
87 90 def test_edit_routing
88 91 assert_routing(
89 92 {:method => :get, :path => '/documents/22/edit'},
90 93 :controller => 'documents', :action => 'edit', :id => '22'
91 94 )
92 95 assert_recognizes(#TODO: should be using PUT on document URI
93 96 {:controller => 'documents', :action => 'edit', :id => '567'},
94 97 {:method => :post, :path => '/documents/567/edit'}
95 98 )
96 99 end
97 100
98 101 def test_show_routing
99 102 assert_routing(
100 103 {:method => :get, :path => '/documents/22'},
101 104 :controller => 'documents', :action => 'show', :id => '22'
102 105 )
103 106 end
104 107
105 108 def test_destroy_routing
106 109 assert_recognizes(#TODO: should be using DELETE on document URI
107 110 {:controller => 'documents', :action => 'destroy', :id => '567'},
108 111 {:method => :post, :path => '/documents/567/destroy'}
109 112 )
110 113 end
111 114
112 115 def test_destroy
113 116 @request.session[:user_id] = 2
114 117 post :destroy, :id => 1
115 118 assert_redirected_to 'projects/ecookbook/documents'
116 119 assert_nil Document.find_by_id(1)
117 120 end
118 121 end
@@ -1,993 +1,1038
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 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < Test::Unit::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :issues,
30 30 :issue_statuses,
31 31 :versions,
32 32 :trackers,
33 33 :projects_trackers,
34 34 :issue_categories,
35 35 :enabled_modules,
36 36 :enumerations,
37 37 :attachments,
38 38 :workflows,
39 39 :custom_fields,
40 40 :custom_values,
41 41 :custom_fields_trackers,
42 42 :time_entries,
43 43 :journals,
44 44 :journal_details
45 45
46 46 def setup
47 47 @controller = IssuesController.new
48 48 @request = ActionController::TestRequest.new
49 49 @response = ActionController::TestResponse.new
50 50 User.current = nil
51 51 end
52 52
53 53 def test_index_routing
54 54 assert_routing(
55 55 {:method => :get, :path => '/issues'},
56 56 :controller => 'issues', :action => 'index'
57 57 )
58 58 end
59 59
60 60 def test_index
61 61 Setting.default_language = 'en'
62 62
63 63 get :index
64 64 assert_response :success
65 65 assert_template 'index.rhtml'
66 66 assert_not_nil assigns(:issues)
67 67 assert_nil assigns(:project)
68 68 assert_tag :tag => 'a', :content => /Can't print recipes/
69 69 assert_tag :tag => 'a', :content => /Subproject issue/
70 70 # private projects hidden
71 71 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
72 72 assert_no_tag :tag => 'a', :content => /Issue on project 2/
73 73 # project column
74 74 assert_tag :tag => 'th', :content => /Project/
75 75 end
76 76
77 77 def test_index_should_not_list_issues_when_module_disabled
78 78 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
79 79 get :index
80 80 assert_response :success
81 81 assert_template 'index.rhtml'
82 82 assert_not_nil assigns(:issues)
83 83 assert_nil assigns(:project)
84 84 assert_no_tag :tag => 'a', :content => /Can't print recipes/
85 85 assert_tag :tag => 'a', :content => /Subproject issue/
86 86 end
87 87
88 88 def test_index_with_project_routing
89 89 assert_routing(
90 90 {:method => :get, :path => '/projects/23/issues'},
91 91 :controller => 'issues', :action => 'index', :project_id => '23'
92 92 )
93 93 end
94 94
95 95 def test_index_should_not_list_issues_when_module_disabled
96 96 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
97 97 get :index
98 98 assert_response :success
99 99 assert_template 'index.rhtml'
100 100 assert_not_nil assigns(:issues)
101 101 assert_nil assigns(:project)
102 102 assert_no_tag :tag => 'a', :content => /Can't print recipes/
103 103 assert_tag :tag => 'a', :content => /Subproject issue/
104 104 end
105 105
106 106 def test_index_with_project_routing
107 107 assert_routing(
108 108 {:method => :get, :path => 'projects/23/issues'},
109 109 :controller => 'issues', :action => 'index', :project_id => '23'
110 110 )
111 111 end
112 112
113 113 def test_index_with_project
114 114 Setting.display_subprojects_issues = 0
115 115 get :index, :project_id => 1
116 116 assert_response :success
117 117 assert_template 'index.rhtml'
118 118 assert_not_nil assigns(:issues)
119 119 assert_tag :tag => 'a', :content => /Can't print recipes/
120 120 assert_no_tag :tag => 'a', :content => /Subproject issue/
121 121 end
122 122
123 123 def test_index_with_project_and_subprojects
124 124 Setting.display_subprojects_issues = 1
125 125 get :index, :project_id => 1
126 126 assert_response :success
127 127 assert_template 'index.rhtml'
128 128 assert_not_nil assigns(:issues)
129 129 assert_tag :tag => 'a', :content => /Can't print recipes/
130 130 assert_tag :tag => 'a', :content => /Subproject issue/
131 131 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
132 132 end
133 133
134 134 def test_index_with_project_and_subprojects_should_show_private_subprojects
135 135 @request.session[:user_id] = 2
136 136 Setting.display_subprojects_issues = 1
137 137 get :index, :project_id => 1
138 138 assert_response :success
139 139 assert_template 'index.rhtml'
140 140 assert_not_nil assigns(:issues)
141 141 assert_tag :tag => 'a', :content => /Can't print recipes/
142 142 assert_tag :tag => 'a', :content => /Subproject issue/
143 143 assert_tag :tag => 'a', :content => /Issue of a private subproject/
144 144 end
145 145
146 146 def test_index_with_project_routing_formatted
147 147 assert_routing(
148 148 {:method => :get, :path => 'projects/23/issues.pdf'},
149 149 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
150 150 )
151 151 assert_routing(
152 152 {:method => :get, :path => 'projects/23/issues.atom'},
153 153 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
154 154 )
155 155 end
156 156
157 157 def test_index_with_project_and_filter
158 158 get :index, :project_id => 1, :set_filter => 1
159 159 assert_response :success
160 160 assert_template 'index.rhtml'
161 161 assert_not_nil assigns(:issues)
162 162 end
163 163
164 164 def test_index_csv_with_project
165 165 get :index, :format => 'csv'
166 166 assert_response :success
167 167 assert_not_nil assigns(:issues)
168 168 assert_equal 'text/csv', @response.content_type
169 169
170 170 get :index, :project_id => 1, :format => 'csv'
171 171 assert_response :success
172 172 assert_not_nil assigns(:issues)
173 173 assert_equal 'text/csv', @response.content_type
174 174 end
175 175
176 176 def test_index_formatted
177 177 assert_routing(
178 178 {:method => :get, :path => 'issues.pdf'},
179 179 :controller => 'issues', :action => 'index', :format => 'pdf'
180 180 )
181 181 assert_routing(
182 182 {:method => :get, :path => 'issues.atom'},
183 183 :controller => 'issues', :action => 'index', :format => 'atom'
184 184 )
185 185 end
186 186
187 187 def test_index_pdf
188 188 get :index, :format => 'pdf'
189 189 assert_response :success
190 190 assert_not_nil assigns(:issues)
191 191 assert_equal 'application/pdf', @response.content_type
192 192
193 193 get :index, :project_id => 1, :format => 'pdf'
194 194 assert_response :success
195 195 assert_not_nil assigns(:issues)
196 196 assert_equal 'application/pdf', @response.content_type
197 197 end
198 198
199 199 def test_index_sort
200 200 get :index, :sort => 'tracker,id:desc'
201 201 assert_response :success
202 202
203 203 sort_params = @request.session['issues_index_sort']
204 204 assert sort_params.is_a?(String)
205 205 assert_equal 'tracker,id:desc', sort_params
206 206
207 207 issues = assigns(:issues)
208 208 assert_not_nil issues
209 209 assert !issues.empty?
210 210 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
211 211 end
212 212
213 213 def test_gantt
214 214 get :gantt, :project_id => 1
215 215 assert_response :success
216 216 assert_template 'gantt.rhtml'
217 217 assert_not_nil assigns(:gantt)
218 218 events = assigns(:gantt).events
219 219 assert_not_nil events
220 220 # Issue with start and due dates
221 221 i = Issue.find(1)
222 222 assert_not_nil i.due_date
223 223 assert events.include?(Issue.find(1))
224 224 # Issue with without due date but targeted to a version with date
225 225 i = Issue.find(2)
226 226 assert_nil i.due_date
227 227 assert events.include?(i)
228 228 end
229 229
230 230 def test_cross_project_gantt
231 231 get :gantt
232 232 assert_response :success
233 233 assert_template 'gantt.rhtml'
234 234 assert_not_nil assigns(:gantt)
235 235 events = assigns(:gantt).events
236 236 assert_not_nil events
237 237 end
238 238
239 239 def test_gantt_export_to_pdf
240 240 get :gantt, :project_id => 1, :format => 'pdf'
241 241 assert_response :success
242 242 assert_equal 'application/pdf', @response.content_type
243 243 assert @response.body.starts_with?('%PDF')
244 244 assert_not_nil assigns(:gantt)
245 245 end
246 246
247 247 def test_cross_project_gantt_export_to_pdf
248 248 get :gantt, :format => 'pdf'
249 249 assert_response :success
250 250 assert_equal 'application/pdf', @response.content_type
251 251 assert @response.body.starts_with?('%PDF')
252 252 assert_not_nil assigns(:gantt)
253 253 end
254 254
255 255 if Object.const_defined?(:Magick)
256 256 def test_gantt_image
257 257 get :gantt, :project_id => 1, :format => 'png'
258 258 assert_response :success
259 259 assert_equal 'image/png', @response.content_type
260 260 end
261 261 else
262 262 puts "RMagick not installed. Skipping tests !!!"
263 263 end
264 264
265 265 def test_calendar
266 266 get :calendar, :project_id => 1
267 267 assert_response :success
268 268 assert_template 'calendar'
269 269 assert_not_nil assigns(:calendar)
270 270 end
271 271
272 272 def test_cross_project_calendar
273 273 get :calendar
274 274 assert_response :success
275 275 assert_template 'calendar'
276 276 assert_not_nil assigns(:calendar)
277 277 end
278 278
279 279 def test_changes
280 280 get :changes, :project_id => 1
281 281 assert_response :success
282 282 assert_not_nil assigns(:journals)
283 283 assert_equal 'application/atom+xml', @response.content_type
284 284 end
285 285
286 286 def test_show_routing
287 287 assert_routing(
288 288 {:method => :get, :path => '/issues/64'},
289 289 :controller => 'issues', :action => 'show', :id => '64'
290 290 )
291 291 end
292 292
293 293 def test_show_routing_formatted
294 294 assert_routing(
295 295 {:method => :get, :path => '/issues/2332.pdf'},
296 296 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
297 297 )
298 298 assert_routing(
299 299 {:method => :get, :path => '/issues/23123.atom'},
300 300 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
301 301 )
302 302 end
303 303
304 304 def test_show_by_anonymous
305 305 get :show, :id => 1
306 306 assert_response :success
307 307 assert_template 'show.rhtml'
308 308 assert_not_nil assigns(:issue)
309 309 assert_equal Issue.find(1), assigns(:issue)
310 310
311 311 # anonymous role is allowed to add a note
312 312 assert_tag :tag => 'form',
313 313 :descendant => { :tag => 'fieldset',
314 314 :child => { :tag => 'legend',
315 315 :content => /Notes/ } }
316 316 end
317 317
318 318 def test_show_by_manager
319 319 @request.session[:user_id] = 2
320 320 get :show, :id => 1
321 321 assert_response :success
322 322
323 323 assert_tag :tag => 'form',
324 324 :descendant => { :tag => 'fieldset',
325 325 :child => { :tag => 'legend',
326 326 :content => /Change properties/ } },
327 327 :descendant => { :tag => 'fieldset',
328 328 :child => { :tag => 'legend',
329 329 :content => /Log time/ } },
330 330 :descendant => { :tag => 'fieldset',
331 331 :child => { :tag => 'legend',
332 332 :content => /Notes/ } }
333 333 end
334 334
335 335 def test_show_should_not_disclose_relations_to_invisible_issues
336 336 Setting.cross_project_issue_relations = '1'
337 337 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
338 338 # Relation to a private project issue
339 339 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
340 340
341 341 get :show, :id => 1
342 342 assert_response :success
343 343
344 344 assert_tag :div, :attributes => { :id => 'relations' },
345 345 :descendant => { :tag => 'a', :content => /#2$/ }
346 346 assert_no_tag :div, :attributes => { :id => 'relations' },
347 347 :descendant => { :tag => 'a', :content => /#4$/ }
348 348 end
349 349
350 350 def test_new_routing
351 351 assert_routing(
352 352 {:method => :get, :path => '/projects/1/issues/new'},
353 353 :controller => 'issues', :action => 'new', :project_id => '1'
354 354 )
355 355 assert_recognizes(
356 356 {:controller => 'issues', :action => 'new', :project_id => '1'},
357 357 {:method => :post, :path => '/projects/1/issues'}
358 358 )
359 359 end
360 360
361 361 def test_show_export_to_pdf
362 362 get :show, :id => 3, :format => 'pdf'
363 363 assert_response :success
364 364 assert_equal 'application/pdf', @response.content_type
365 365 assert @response.body.starts_with?('%PDF')
366 366 assert_not_nil assigns(:issue)
367 367 end
368 368
369 369 def test_get_new
370 370 @request.session[:user_id] = 2
371 371 get :new, :project_id => 1, :tracker_id => 1
372 372 assert_response :success
373 373 assert_template 'new'
374 374
375 375 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
376 376 :value => 'Default string' }
377 377 end
378 378
379 379 def test_get_new_without_tracker_id
380 380 @request.session[:user_id] = 2
381 381 get :new, :project_id => 1
382 382 assert_response :success
383 383 assert_template 'new'
384 384
385 385 issue = assigns(:issue)
386 386 assert_not_nil issue
387 387 assert_equal Project.find(1).trackers.first, issue.tracker
388 388 end
389 389
390 390 def test_get_new_with_no_default_status_should_display_an_error
391 391 @request.session[:user_id] = 2
392 392 IssueStatus.delete_all
393 393
394 394 get :new, :project_id => 1
395 395 assert_response 500
396 396 assert_not_nil flash[:error]
397 397 assert_tag :tag => 'div', :attributes => { :class => /error/ },
398 398 :content => /No default issue/
399 399 end
400 400
401 401 def test_get_new_with_no_tracker_should_display_an_error
402 402 @request.session[:user_id] = 2
403 403 Tracker.delete_all
404 404
405 405 get :new, :project_id => 1
406 406 assert_response 500
407 407 assert_not_nil flash[:error]
408 408 assert_tag :tag => 'div', :attributes => { :class => /error/ },
409 409 :content => /No tracker/
410 410 end
411 411
412 412 def test_update_new_form
413 413 @request.session[:user_id] = 2
414 414 xhr :post, :new, :project_id => 1,
415 415 :issue => {:tracker_id => 2,
416 416 :subject => 'This is the test_new issue',
417 417 :description => 'This is the description',
418 418 :priority_id => 5}
419 419 assert_response :success
420 420 assert_template 'new'
421 421 end
422 422
423 423 def test_post_new
424 424 @request.session[:user_id] = 2
425 425 post :new, :project_id => 1,
426 426 :issue => {:tracker_id => 3,
427 427 :subject => 'This is the test_new issue',
428 428 :description => 'This is the description',
429 429 :priority_id => 5,
430 430 :estimated_hours => '',
431 431 :custom_field_values => {'2' => 'Value for field 2'}}
432 432 assert_redirected_to :action => 'show'
433 433
434 434 issue = Issue.find_by_subject('This is the test_new issue')
435 435 assert_not_nil issue
436 436 assert_equal 2, issue.author_id
437 437 assert_equal 3, issue.tracker_id
438 438 assert_nil issue.estimated_hours
439 439 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
440 440 assert_not_nil v
441 441 assert_equal 'Value for field 2', v.value
442 442 end
443 443
444 444 def test_post_new_and_continue
445 445 @request.session[:user_id] = 2
446 446 post :new, :project_id => 1,
447 447 :issue => {:tracker_id => 3,
448 448 :subject => 'This is first issue',
449 449 :priority_id => 5},
450 450 :continue => ''
451 451 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
452 452 end
453 453
454 454 def test_post_new_without_custom_fields_param
455 455 @request.session[:user_id] = 2
456 456 post :new, :project_id => 1,
457 457 :issue => {:tracker_id => 1,
458 458 :subject => 'This is the test_new issue',
459 459 :description => 'This is the description',
460 460 :priority_id => 5}
461 461 assert_redirected_to :action => 'show'
462 462 end
463 463
464 464 def test_post_new_with_required_custom_field_and_without_custom_fields_param
465 465 field = IssueCustomField.find_by_name('Database')
466 466 field.update_attribute(:is_required, true)
467 467
468 468 @request.session[:user_id] = 2
469 469 post :new, :project_id => 1,
470 470 :issue => {:tracker_id => 1,
471 471 :subject => 'This is the test_new issue',
472 472 :description => 'This is the description',
473 473 :priority_id => 5}
474 474 assert_response :success
475 475 assert_template 'new'
476 476 issue = assigns(:issue)
477 477 assert_not_nil issue
478 478 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
479 479 end
480 480
481 481 def test_post_new_with_watchers
482 482 @request.session[:user_id] = 2
483 483 ActionMailer::Base.deliveries.clear
484 484
485 485 assert_difference 'Watcher.count', 2 do
486 486 post :new, :project_id => 1,
487 487 :issue => {:tracker_id => 1,
488 488 :subject => 'This is a new issue with watchers',
489 489 :description => 'This is the description',
490 490 :priority_id => 5,
491 491 :watcher_user_ids => ['2', '3']}
492 492 end
493 493 issue = Issue.find_by_subject('This is a new issue with watchers')
494 494 assert_not_nil issue
495 495 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
496 496
497 497 # Watchers added
498 498 assert_equal [2, 3], issue.watcher_user_ids.sort
499 499 assert issue.watched_by?(User.find(3))
500 500 # Watchers notified
501 501 mail = ActionMailer::Base.deliveries.last
502 502 assert_kind_of TMail::Mail, mail
503 503 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
504 504 end
505 505
506 def test_post_new_should_send_a_notification
507 ActionMailer::Base.deliveries.clear
508 @request.session[:user_id] = 2
509 post :new, :project_id => 1,
510 :issue => {:tracker_id => 3,
511 :subject => 'This is the test_new issue',
512 :description => 'This is the description',
513 :priority_id => 5,
514 :estimated_hours => '',
515 :custom_field_values => {'2' => 'Value for field 2'}}
516 assert_redirected_to :action => 'show'
517
518 assert_equal 1, ActionMailer::Base.deliveries.size
519 end
520
506 521 def test_post_should_preserve_fields_values_on_validation_failure
507 522 @request.session[:user_id] = 2
508 523 post :new, :project_id => 1,
509 524 :issue => {:tracker_id => 1,
510 525 # empty subject
511 526 :subject => '',
512 527 :description => 'This is a description',
513 528 :priority_id => 6,
514 529 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
515 530 assert_response :success
516 531 assert_template 'new'
517 532
518 533 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
519 534 :content => 'This is a description'
520 535 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
521 536 :child => { :tag => 'option', :attributes => { :selected => 'selected',
522 537 :value => '6' },
523 538 :content => 'High' }
524 539 # Custom fields
525 540 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
526 541 :child => { :tag => 'option', :attributes => { :selected => 'selected',
527 542 :value => 'Oracle' },
528 543 :content => 'Oracle' }
529 544 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
530 545 :value => 'Value for field 2'}
531 546 end
532 547
533 548 def test_copy_routing
534 549 assert_routing(
535 550 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
536 551 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
537 552 )
538 553 end
539 554
540 555 def test_copy_issue
541 556 @request.session[:user_id] = 2
542 557 get :new, :project_id => 1, :copy_from => 1
543 558 assert_template 'new'
544 559 assert_not_nil assigns(:issue)
545 560 orig = Issue.find(1)
546 561 assert_equal orig.subject, assigns(:issue).subject
547 562 end
548 563
549 564 def test_edit_routing
550 565 assert_routing(
551 566 {:method => :get, :path => '/issues/1/edit'},
552 567 :controller => 'issues', :action => 'edit', :id => '1'
553 568 )
554 569 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
555 570 {:controller => 'issues', :action => 'edit', :id => '1'},
556 571 {:method => :post, :path => '/issues/1/edit'}
557 572 )
558 573 end
559 574
560 575 def test_get_edit
561 576 @request.session[:user_id] = 2
562 577 get :edit, :id => 1
563 578 assert_response :success
564 579 assert_template 'edit'
565 580 assert_not_nil assigns(:issue)
566 581 assert_equal Issue.find(1), assigns(:issue)
567 582 end
568 583
569 584 def test_get_edit_with_params
570 585 @request.session[:user_id] = 2
571 586 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
572 587 assert_response :success
573 588 assert_template 'edit'
574 589
575 590 issue = assigns(:issue)
576 591 assert_not_nil issue
577 592
578 593 assert_equal 5, issue.status_id
579 594 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
580 595 :child => { :tag => 'option',
581 596 :content => 'Closed',
582 597 :attributes => { :selected => 'selected' } }
583 598
584 599 assert_equal 7, issue.priority_id
585 600 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
586 601 :child => { :tag => 'option',
587 602 :content => 'Urgent',
588 603 :attributes => { :selected => 'selected' } }
589 604 end
590 605
591 606 def test_reply_routing
592 607 assert_routing(
593 608 {:method => :post, :path => '/issues/1/quoted'},
594 609 :controller => 'issues', :action => 'reply', :id => '1'
595 610 )
596 611 end
597 612
598 613 def test_reply_to_issue
599 614 @request.session[:user_id] = 2
600 615 get :reply, :id => 1
601 616 assert_response :success
602 617 assert_select_rjs :show, "update"
603 618 end
604 619
605 620 def test_reply_to_note
606 621 @request.session[:user_id] = 2
607 622 get :reply, :id => 1, :journal_id => 2
608 623 assert_response :success
609 624 assert_select_rjs :show, "update"
610 625 end
611 626
612 627 def test_post_edit_without_custom_fields_param
613 628 @request.session[:user_id] = 2
614 629 ActionMailer::Base.deliveries.clear
615 630
616 631 issue = Issue.find(1)
617 632 assert_equal '125', issue.custom_value_for(2).value
618 633 old_subject = issue.subject
619 634 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
620 635
621 636 assert_difference('Journal.count') do
622 637 assert_difference('JournalDetail.count', 2) do
623 638 post :edit, :id => 1, :issue => {:subject => new_subject,
624 639 :priority_id => '6',
625 640 :category_id => '1' # no change
626 641 }
627 642 end
628 643 end
629 644 assert_redirected_to :action => 'show', :id => '1'
630 645 issue.reload
631 646 assert_equal new_subject, issue.subject
632 647 # Make sure custom fields were not cleared
633 648 assert_equal '125', issue.custom_value_for(2).value
634 649
635 650 mail = ActionMailer::Base.deliveries.last
636 651 assert_kind_of TMail::Mail, mail
637 652 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
638 653 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
639 654 end
640 655
641 656 def test_post_edit_with_custom_field_change
642 657 @request.session[:user_id] = 2
643 658 issue = Issue.find(1)
644 659 assert_equal '125', issue.custom_value_for(2).value
645 660
646 661 assert_difference('Journal.count') do
647 662 assert_difference('JournalDetail.count', 3) do
648 663 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
649 664 :priority_id => '6',
650 665 :category_id => '1', # no change
651 666 :custom_field_values => { '2' => 'New custom value' }
652 667 }
653 668 end
654 669 end
655 670 assert_redirected_to :action => 'show', :id => '1'
656 671 issue.reload
657 672 assert_equal 'New custom value', issue.custom_value_for(2).value
658 673
659 674 mail = ActionMailer::Base.deliveries.last
660 675 assert_kind_of TMail::Mail, mail
661 676 assert mail.body.include?("Searchable field changed from 125 to New custom value")
662 677 end
663 678
664 679 def test_post_edit_with_status_and_assignee_change
665 680 issue = Issue.find(1)
666 681 assert_equal 1, issue.status_id
667 682 @request.session[:user_id] = 2
668 683 assert_difference('TimeEntry.count', 0) do
669 684 post :edit,
670 685 :id => 1,
671 686 :issue => { :status_id => 2, :assigned_to_id => 3 },
672 687 :notes => 'Assigned to dlopper',
673 688 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.activities.first }
674 689 end
675 690 assert_redirected_to :action => 'show', :id => '1'
676 691 issue.reload
677 692 assert_equal 2, issue.status_id
678 693 j = issue.journals.find(:first, :order => 'id DESC')
679 694 assert_equal 'Assigned to dlopper', j.notes
680 695 assert_equal 2, j.details.size
681 696
682 697 mail = ActionMailer::Base.deliveries.last
683 698 assert mail.body.include?("Status changed from New to Assigned")
684 699 end
685 700
686 701 def test_post_edit_with_note_only
687 702 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
688 703 # anonymous user
689 704 post :edit,
690 705 :id => 1,
691 706 :notes => notes
692 707 assert_redirected_to :action => 'show', :id => '1'
693 708 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
694 709 assert_equal notes, j.notes
695 710 assert_equal 0, j.details.size
696 711 assert_equal User.anonymous, j.user
697 712
698 713 mail = ActionMailer::Base.deliveries.last
699 714 assert mail.body.include?(notes)
700 715 end
701 716
702 717 def test_post_edit_with_note_and_spent_time
703 718 @request.session[:user_id] = 2
704 719 spent_hours_before = Issue.find(1).spent_hours
705 720 assert_difference('TimeEntry.count') do
706 721 post :edit,
707 722 :id => 1,
708 723 :notes => '2.5 hours added',
709 724 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.activities.first }
710 725 end
711 726 assert_redirected_to :action => 'show', :id => '1'
712 727
713 728 issue = Issue.find(1)
714 729
715 730 j = issue.journals.find(:first, :order => 'id DESC')
716 731 assert_equal '2.5 hours added', j.notes
717 732 assert_equal 0, j.details.size
718 733
719 734 t = issue.time_entries.find(:first, :order => 'id DESC')
720 735 assert_not_nil t
721 736 assert_equal 2.5, t.hours
722 737 assert_equal spent_hours_before + 2.5, issue.spent_hours
723 738 end
724 739
725 740 def test_post_edit_with_attachment_only
726 741 set_tmp_attachments_directory
727 742
728 743 # Delete all fixtured journals, a race condition can occur causing the wrong
729 744 # journal to get fetched in the next find.
730 745 Journal.delete_all
731 746
732 747 # anonymous user
733 748 post :edit,
734 749 :id => 1,
735 750 :notes => '',
736 751 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
737 752 assert_redirected_to :action => 'show', :id => '1'
738 753 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
739 754 assert j.notes.blank?
740 755 assert_equal 1, j.details.size
741 756 assert_equal 'testfile.txt', j.details.first.value
742 757 assert_equal User.anonymous, j.user
743 758
744 759 mail = ActionMailer::Base.deliveries.last
745 760 assert mail.body.include?('testfile.txt')
746 761 end
747 762
748 763 def test_post_edit_with_no_change
749 764 issue = Issue.find(1)
750 765 issue.journals.clear
751 766 ActionMailer::Base.deliveries.clear
752 767
753 768 post :edit,
754 769 :id => 1,
755 770 :notes => ''
756 771 assert_redirected_to :action => 'show', :id => '1'
757 772
758 773 issue.reload
759 774 assert issue.journals.empty?
760 775 # No email should be sent
761 776 assert ActionMailer::Base.deliveries.empty?
762 777 end
778
779 def test_post_edit_should_send_a_notification
780 @request.session[:user_id] = 2
781 ActionMailer::Base.deliveries.clear
782 issue = Issue.find(1)
783 old_subject = issue.subject
784 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
785
786 post :edit, :id => 1, :issue => {:subject => new_subject,
787 :priority_id => '6',
788 :category_id => '1' # no change
789 }
790 assert_equal 1, ActionMailer::Base.deliveries.size
791 end
763 792
764 793 def test_post_edit_with_invalid_spent_time
765 794 @request.session[:user_id] = 2
766 795 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
767 796
768 797 assert_no_difference('Journal.count') do
769 798 post :edit,
770 799 :id => 1,
771 800 :notes => notes,
772 801 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
773 802 end
774 803 assert_response :success
775 804 assert_template 'edit'
776 805
777 806 assert_tag :textarea, :attributes => { :name => 'notes' },
778 807 :content => notes
779 808 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
780 809 end
781 810
782 811 def test_bulk_edit
783 812 @request.session[:user_id] = 2
784 813 # update issues priority
785 814 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
786 815 :assigned_to_id => '',
787 816 :custom_field_values => {'2' => ''},
788 817 :notes => 'Bulk editing'
789 818 assert_response 302
790 819 # check that the issues were updated
791 820 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
792 821
793 822 issue = Issue.find(1)
794 823 journal = issue.journals.find(:first, :order => 'created_on DESC')
795 824 assert_equal '125', issue.custom_value_for(2).value
796 825 assert_equal 'Bulk editing', journal.notes
797 826 assert_equal 1, journal.details.size
798 827 end
799 828
829 def test_bullk_edit_should_send_a_notification
830 @request.session[:user_id] = 2
831 ActionMailer::Base.deliveries.clear
832 post(:bulk_edit,
833 {
834 :ids => [1, 2],
835 :priority_id => 7,
836 :assigned_to_id => '',
837 :custom_field_values => {'2' => ''},
838 :notes => 'Bulk editing'
839 })
840
841 assert_response 302
842 assert_equal 2, ActionMailer::Base.deliveries.size
843 end
844
800 845 def test_bulk_edit_custom_field
801 846 @request.session[:user_id] = 2
802 847 # update issues priority
803 848 post :bulk_edit, :ids => [1, 2], :priority_id => '',
804 849 :assigned_to_id => '',
805 850 :custom_field_values => {'2' => '777'},
806 851 :notes => 'Bulk editing custom field'
807 852 assert_response 302
808 853
809 854 issue = Issue.find(1)
810 855 journal = issue.journals.find(:first, :order => 'created_on DESC')
811 856 assert_equal '777', issue.custom_value_for(2).value
812 857 assert_equal 1, journal.details.size
813 858 assert_equal '125', journal.details.first.old_value
814 859 assert_equal '777', journal.details.first.value
815 860 end
816 861
817 862 def test_bulk_unassign
818 863 assert_not_nil Issue.find(2).assigned_to
819 864 @request.session[:user_id] = 2
820 865 # unassign issues
821 866 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
822 867 assert_response 302
823 868 # check that the issues were updated
824 869 assert_nil Issue.find(2).assigned_to
825 870 end
826 871
827 872 def test_move_routing
828 873 assert_routing(
829 874 {:method => :get, :path => '/issues/1/move'},
830 875 :controller => 'issues', :action => 'move', :id => '1'
831 876 )
832 877 assert_recognizes(
833 878 {:controller => 'issues', :action => 'move', :id => '1'},
834 879 {:method => :post, :path => '/issues/1/move'}
835 880 )
836 881 end
837 882
838 883 def test_move_one_issue_to_another_project
839 884 @request.session[:user_id] = 1
840 885 post :move, :id => 1, :new_project_id => 2
841 886 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
842 887 assert_equal 2, Issue.find(1).project_id
843 888 end
844 889
845 890 def test_bulk_move_to_another_project
846 891 @request.session[:user_id] = 1
847 892 post :move, :ids => [1, 2], :new_project_id => 2
848 893 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
849 894 # Issues moved to project 2
850 895 assert_equal 2, Issue.find(1).project_id
851 896 assert_equal 2, Issue.find(2).project_id
852 897 # No tracker change
853 898 assert_equal 1, Issue.find(1).tracker_id
854 899 assert_equal 2, Issue.find(2).tracker_id
855 900 end
856 901
857 902 def test_bulk_move_to_another_tracker
858 903 @request.session[:user_id] = 1
859 904 post :move, :ids => [1, 2], :new_tracker_id => 2
860 905 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
861 906 assert_equal 2, Issue.find(1).tracker_id
862 907 assert_equal 2, Issue.find(2).tracker_id
863 908 end
864 909
865 910 def test_bulk_copy_to_another_project
866 911 @request.session[:user_id] = 1
867 912 assert_difference 'Issue.count', 2 do
868 913 assert_no_difference 'Project.find(1).issues.count' do
869 914 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
870 915 end
871 916 end
872 917 assert_redirected_to 'projects/ecookbook/issues'
873 918 end
874 919
875 920 def test_context_menu_one_issue
876 921 @request.session[:user_id] = 2
877 922 get :context_menu, :ids => [1]
878 923 assert_response :success
879 924 assert_template 'context_menu'
880 925 assert_tag :tag => 'a', :content => 'Edit',
881 926 :attributes => { :href => '/issues/1/edit',
882 927 :class => 'icon-edit' }
883 928 assert_tag :tag => 'a', :content => 'Closed',
884 929 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
885 930 :class => '' }
886 931 assert_tag :tag => 'a', :content => 'Immediate',
887 932 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
888 933 :class => '' }
889 934 assert_tag :tag => 'a', :content => 'Dave Lopper',
890 935 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
891 936 :class => '' }
892 937 assert_tag :tag => 'a', :content => 'Copy',
893 938 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
894 939 :class => 'icon-copy' }
895 940 assert_tag :tag => 'a', :content => 'Move',
896 941 :attributes => { :href => '/issues/move?ids%5B%5D=1',
897 942 :class => 'icon-move' }
898 943 assert_tag :tag => 'a', :content => 'Delete',
899 944 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
900 945 :class => 'icon-del' }
901 946 end
902 947
903 948 def test_context_menu_one_issue_by_anonymous
904 949 get :context_menu, :ids => [1]
905 950 assert_response :success
906 951 assert_template 'context_menu'
907 952 assert_tag :tag => 'a', :content => 'Delete',
908 953 :attributes => { :href => '#',
909 954 :class => 'icon-del disabled' }
910 955 end
911 956
912 957 def test_context_menu_multiple_issues_of_same_project
913 958 @request.session[:user_id] = 2
914 959 get :context_menu, :ids => [1, 2]
915 960 assert_response :success
916 961 assert_template 'context_menu'
917 962 assert_tag :tag => 'a', :content => 'Edit',
918 963 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
919 964 :class => 'icon-edit' }
920 965 assert_tag :tag => 'a', :content => 'Immediate',
921 966 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
922 967 :class => '' }
923 968 assert_tag :tag => 'a', :content => 'Dave Lopper',
924 969 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
925 970 :class => '' }
926 971 assert_tag :tag => 'a', :content => 'Move',
927 972 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
928 973 :class => 'icon-move' }
929 974 assert_tag :tag => 'a', :content => 'Delete',
930 975 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
931 976 :class => 'icon-del' }
932 977 end
933 978
934 979 def test_context_menu_multiple_issues_of_different_project
935 980 @request.session[:user_id] = 2
936 981 get :context_menu, :ids => [1, 2, 4]
937 982 assert_response :success
938 983 assert_template 'context_menu'
939 984 assert_tag :tag => 'a', :content => 'Delete',
940 985 :attributes => { :href => '#',
941 986 :class => 'icon-del disabled' }
942 987 end
943 988
944 989 def test_destroy_routing
945 990 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
946 991 {:controller => 'issues', :action => 'destroy', :id => '1'},
947 992 {:method => :post, :path => '/issues/1/destroy'}
948 993 )
949 994 end
950 995
951 996 def test_destroy_issue_with_no_time_entries
952 997 assert_nil TimeEntry.find_by_issue_id(2)
953 998 @request.session[:user_id] = 2
954 999 post :destroy, :id => 2
955 1000 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
956 1001 assert_nil Issue.find_by_id(2)
957 1002 end
958 1003
959 1004 def test_destroy_issues_with_time_entries
960 1005 @request.session[:user_id] = 2
961 1006 post :destroy, :ids => [1, 3]
962 1007 assert_response :success
963 1008 assert_template 'destroy'
964 1009 assert_not_nil assigns(:hours)
965 1010 assert Issue.find_by_id(1) && Issue.find_by_id(3)
966 1011 end
967 1012
968 1013 def test_destroy_issues_and_destroy_time_entries
969 1014 @request.session[:user_id] = 2
970 1015 post :destroy, :ids => [1, 3], :todo => 'destroy'
971 1016 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
972 1017 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
973 1018 assert_nil TimeEntry.find_by_id([1, 2])
974 1019 end
975 1020
976 1021 def test_destroy_issues_and_assign_time_entries_to_project
977 1022 @request.session[:user_id] = 2
978 1023 post :destroy, :ids => [1, 3], :todo => 'nullify'
979 1024 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
980 1025 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
981 1026 assert_nil TimeEntry.find(1).issue_id
982 1027 assert_nil TimeEntry.find(2).issue_id
983 1028 end
984 1029
985 1030 def test_destroy_issues_and_reassign_time_entries_to_another_issue
986 1031 @request.session[:user_id] = 2
987 1032 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
988 1033 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
989 1034 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
990 1035 assert_equal 2, TimeEntry.find(1).issue_id
991 1036 assert_equal 2, TimeEntry.find(2).issue_id
992 1037 end
993 1038 end
@@ -1,211 +1,215
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 require 'news_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class NewsController; def rescue_action(e) raise e end; end
23 23
24 24 class NewsControllerTest < Test::Unit::TestCase
25 25 fixtures :projects, :users, :roles, :members, :enabled_modules, :news, :comments
26 26
27 27 def setup
28 28 @controller = NewsController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_index_routing
35 35 assert_routing(
36 36 {:method => :get, :path => '/news'},
37 37 :controller => 'news', :action => 'index'
38 38 )
39 39 end
40 40
41 41 def test_index_routing_formatted
42 42 assert_routing(
43 43 {:method => :get, :path => '/news.atom'},
44 44 :controller => 'news', :action => 'index', :format => 'atom'
45 45 )
46 46 end
47 47
48 48 def test_index
49 49 get :index
50 50 assert_response :success
51 51 assert_template 'index'
52 52 assert_not_nil assigns(:newss)
53 53 assert_nil assigns(:project)
54 54 end
55 55
56 56 def test_index_with_project_routing
57 57 assert_routing(
58 58 {:method => :get, :path => '/projects/567/news'},
59 59 :controller => 'news', :action => 'index', :project_id => '567'
60 60 )
61 61 end
62 62
63 63 def test_index_with_project_routing_formatted
64 64 assert_routing(
65 65 {:method => :get, :path => '/projects/567/news.atom'},
66 66 :controller => 'news', :action => 'index', :project_id => '567', :format => 'atom'
67 67 )
68 68 end
69 69
70 70 def test_index_with_project
71 71 get :index, :project_id => 1
72 72 assert_response :success
73 73 assert_template 'index'
74 74 assert_not_nil assigns(:newss)
75 75 end
76 76
77 77 def test_show_routing
78 78 assert_routing(
79 79 {:method => :get, :path => '/news/2'},
80 80 :controller => 'news', :action => 'show', :id => '2'
81 81 )
82 82 end
83 83
84 84 def test_show
85 85 get :show, :id => 1
86 86 assert_response :success
87 87 assert_template 'show'
88 88 assert_tag :tag => 'h2', :content => /eCookbook first release/
89 89 end
90 90
91 91 def test_show_not_found
92 92 get :show, :id => 999
93 93 assert_response 404
94 94 end
95 95
96 96 def test_new_routing
97 97 assert_routing(
98 98 {:method => :get, :path => '/projects/567/news/new'},
99 99 :controller => 'news', :action => 'new', :project_id => '567'
100 100 )
101 101 assert_recognizes(
102 102 {:controller => 'news', :action => 'new', :project_id => '567'},
103 103 {:method => :post, :path => '/projects/567/news'}
104 104 )
105 105 end
106 106
107 107 def test_get_new
108 108 @request.session[:user_id] = 2
109 109 get :new, :project_id => 1
110 110 assert_response :success
111 111 assert_template 'new'
112 112 end
113 113
114 114 def test_post_new
115 ActionMailer::Base.deliveries.clear
116 Setting.notified_events << 'news_added'
117
115 118 @request.session[:user_id] = 2
116 119 post :new, :project_id => 1, :news => { :title => 'NewsControllerTest',
117 120 :description => 'This is the description',
118 121 :summary => '' }
119 122 assert_redirected_to 'projects/ecookbook/news'
120 123
121 124 news = News.find_by_title('NewsControllerTest')
122 125 assert_not_nil news
123 126 assert_equal 'This is the description', news.description
124 127 assert_equal User.find(2), news.author
125 128 assert_equal Project.find(1), news.project
129 assert_equal 1, ActionMailer::Base.deliveries.size
126 130 end
127 131
128 132 def test_edit_routing
129 133 assert_routing(
130 134 {:method => :get, :path => '/news/234'},
131 135 :controller => 'news', :action => 'show', :id => '234'
132 136 )
133 137 assert_recognizes(#TODO: PUT to news URI instead, need to modify form
134 138 {:controller => 'news', :action => 'edit', :id => '567'},
135 139 {:method => :post, :path => '/news/567/edit'}
136 140 )
137 141 end
138 142
139 143 def test_get_edit
140 144 @request.session[:user_id] = 2
141 145 get :edit, :id => 1
142 146 assert_response :success
143 147 assert_template 'edit'
144 148 end
145 149
146 150 def test_post_edit
147 151 @request.session[:user_id] = 2
148 152 post :edit, :id => 1, :news => { :description => 'Description changed by test_post_edit' }
149 153 assert_redirected_to 'news/1'
150 154 news = News.find(1)
151 155 assert_equal 'Description changed by test_post_edit', news.description
152 156 end
153 157
154 158 def test_post_new_with_validation_failure
155 159 @request.session[:user_id] = 2
156 160 post :new, :project_id => 1, :news => { :title => '',
157 161 :description => 'This is the description',
158 162 :summary => '' }
159 163 assert_response :success
160 164 assert_template 'new'
161 165 assert_not_nil assigns(:news)
162 166 assert assigns(:news).new_record?
163 167 assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' },
164 168 :content => /1 error/
165 169 end
166 170
167 171 def test_add_comment
168 172 @request.session[:user_id] = 2
169 173 post :add_comment, :id => 1, :comment => { :comments => 'This is a NewsControllerTest comment' }
170 174 assert_redirected_to 'news/1'
171 175
172 176 comment = News.find(1).comments.find(:first, :order => 'created_on DESC')
173 177 assert_not_nil comment
174 178 assert_equal 'This is a NewsControllerTest comment', comment.comments
175 179 assert_equal User.find(2), comment.author
176 180 end
177 181
178 182 def test_destroy_comment
179 183 comments_count = News.find(1).comments.size
180 184 @request.session[:user_id] = 2
181 185 post :destroy_comment, :id => 1, :comment_id => 2
182 186 assert_redirected_to 'news/1'
183 187 assert_nil Comment.find_by_id(2)
184 188 assert_equal comments_count - 1, News.find(1).comments.size
185 189 end
186 190
187 191 def test_destroy_routing
188 192 assert_recognizes(#TODO: should use DELETE to news URI, need to change form
189 193 {:controller => 'news', :action => 'destroy', :id => '567'},
190 194 {:method => :post, :path => '/news/567/destroy'}
191 195 )
192 196 end
193 197
194 198 def test_destroy
195 199 @request.session[:user_id] = 2
196 200 post :destroy, :id => 1
197 201 assert_redirected_to 'projects/ecookbook/news'
198 202 assert_nil News.find_by_id(1)
199 203 end
200 204
201 205 def test_preview
202 206 get :preview, :project_id => 1,
203 207 :news => {:title => '',
204 208 :description => 'News description',
205 209 :summary => ''}
206 210 assert_response :success
207 211 assert_template 'common/_preview'
208 212 assert_tag :tag => 'fieldset', :attributes => { :class => 'preview' },
209 213 :content => /News description/
210 214 end
211 215 end
@@ -1,73 +1,75
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 ChangesetTest < Test::Unit::TestCase
21 21 fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :trackers
22 22
23 23 def setup
24 24 end
25 25
26 26 def test_ref_keywords_any
27 ActionMailer::Base.deliveries.clear
27 28 Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id
28 29 Setting.commit_fix_done_ratio = '90'
29 30 Setting.commit_ref_keywords = '*'
30 31 Setting.commit_fix_keywords = 'fixes , closes'
31 32
32 33 c = Changeset.new(:repository => Project.find(1).repository,
33 34 :committed_on => Time.now,
34 35 :comments => 'New commit (#2). Fixes #1')
35 36 c.scan_comment_for_issue_ids
36 37
37 38 assert_equal [1, 2], c.issue_ids.sort
38 39 fixed = Issue.find(1)
39 40 assert fixed.closed?
40 41 assert_equal 90, fixed.done_ratio
42 assert_equal 1, ActionMailer::Base.deliveries.size
41 43 end
42 44
43 45 def test_ref_keywords_any_line_start
44 46 Setting.commit_ref_keywords = '*'
45 47
46 48 c = Changeset.new(:repository => Project.find(1).repository,
47 49 :committed_on => Time.now,
48 50 :comments => '#1 is the reason of this commit')
49 51 c.scan_comment_for_issue_ids
50 52
51 53 assert_equal [1], c.issue_ids.sort
52 54 end
53 55
54 56 def test_previous
55 57 changeset = Changeset.find_by_revision('3')
56 58 assert_equal Changeset.find_by_revision('2'), changeset.previous
57 59 end
58 60
59 61 def test_previous_nil
60 62 changeset = Changeset.find_by_revision('1')
61 63 assert_nil changeset.previous
62 64 end
63 65
64 66 def test_next
65 67 changeset = Changeset.find_by_revision('2')
66 68 assert_equal Changeset.find_by_revision('3'), changeset.next
67 69 end
68 70
69 71 def test_next_nil
70 72 changeset = Changeset.find_by_revision('4')
71 73 assert_nil changeset.next
72 74 end
73 75 end
@@ -1,37 +1,46
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 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class DocumentTest < Test::Unit::TestCase
21 21 fixtures :projects, :enumerations, :documents
22 22
23 23 def test_create
24 24 doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation'))
25 25 assert doc.save
26 26 end
27 27
28 def test_create_should_send_email_notification
29 ActionMailer::Base.deliveries.clear
30 Setting.notified_events << 'document_added'
31 doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation'))
32
33 assert doc.save
34 assert_equal 1, ActionMailer::Base.deliveries.size
35 end
36
28 37 def test_create_with_default_category
29 38 # Sets a default category
30 39 e = Enumeration.find_by_name('Technical documentation')
31 40 e.update_attributes(:is_default => true)
32 41
33 42 doc = Document.new(:project => Project.find(1), :title => 'New document')
34 43 assert_equal e, doc.category
35 44 assert doc.save
36 45 end
37 46 end
@@ -1,244 +1,252
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 IssueTest < Test::Unit::TestCase
21 21 fixtures :projects, :users, :members,
22 22 :trackers, :projects_trackers,
23 23 :issue_statuses, :issue_categories,
24 24 :enumerations,
25 25 :issues,
26 26 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 27 :time_entries
28 28
29 29 def test_create
30 30 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
31 31 assert issue.save
32 32 issue.reload
33 33 assert_equal 1.5, issue.estimated_hours
34 34 end
35 35
36 36 def test_create_minimal
37 37 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create')
38 38 assert issue.save
39 39 assert issue.description.nil?
40 40 end
41 41
42 42 def test_create_with_required_custom_field
43 43 field = IssueCustomField.find_by_name('Database')
44 44 field.update_attribute(:is_required, true)
45 45
46 46 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
47 47 assert issue.available_custom_fields.include?(field)
48 48 # No value for the custom field
49 49 assert !issue.save
50 50 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
51 51 # Blank value
52 52 issue.custom_field_values = { field.id => '' }
53 53 assert !issue.save
54 54 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
55 55 # Invalid value
56 56 issue.custom_field_values = { field.id => 'SQLServer' }
57 57 assert !issue.save
58 58 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
59 59 # Valid value
60 60 issue.custom_field_values = { field.id => 'PostgreSQL' }
61 61 assert issue.save
62 62 issue.reload
63 63 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
64 64 end
65 65
66 66 def test_errors_full_messages_should_include_custom_fields_errors
67 67 field = IssueCustomField.find_by_name('Database')
68 68
69 69 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
70 70 assert issue.available_custom_fields.include?(field)
71 71 # Invalid value
72 72 issue.custom_field_values = { field.id => 'SQLServer' }
73 73
74 74 assert !issue.valid?
75 75 assert_equal 1, issue.errors.full_messages.size
76 76 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
77 77 end
78 78
79 79 def test_update_issue_with_required_custom_field
80 80 field = IssueCustomField.find_by_name('Database')
81 81 field.update_attribute(:is_required, true)
82 82
83 83 issue = Issue.find(1)
84 84 assert_nil issue.custom_value_for(field)
85 85 assert issue.available_custom_fields.include?(field)
86 86 # No change to custom values, issue can be saved
87 87 assert issue.save
88 88 # Blank value
89 89 issue.custom_field_values = { field.id => '' }
90 90 assert !issue.save
91 91 # Valid value
92 92 issue.custom_field_values = { field.id => 'PostgreSQL' }
93 93 assert issue.save
94 94 issue.reload
95 95 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
96 96 end
97 97
98 98 def test_should_not_update_attributes_if_custom_fields_validation_fails
99 99 issue = Issue.find(1)
100 100 field = IssueCustomField.find_by_name('Database')
101 101 assert issue.available_custom_fields.include?(field)
102 102
103 103 issue.custom_field_values = { field.id => 'Invalid' }
104 104 issue.subject = 'Should be not be saved'
105 105 assert !issue.save
106 106
107 107 issue.reload
108 108 assert_equal "Can't print recipes", issue.subject
109 109 end
110 110
111 111 def test_should_not_recreate_custom_values_objects_on_update
112 112 field = IssueCustomField.find_by_name('Database')
113 113
114 114 issue = Issue.find(1)
115 115 issue.custom_field_values = { field.id => 'PostgreSQL' }
116 116 assert issue.save
117 117 custom_value = issue.custom_value_for(field)
118 118 issue.reload
119 119 issue.custom_field_values = { field.id => 'MySQL' }
120 120 assert issue.save
121 121 issue.reload
122 122 assert_equal custom_value.id, issue.custom_value_for(field).id
123 123 end
124 124
125 125 def test_category_based_assignment
126 126 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
127 127 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
128 128 end
129 129
130 130 def test_copy
131 131 issue = Issue.new.copy_from(1)
132 132 assert issue.save
133 133 issue.reload
134 134 orig = Issue.find(1)
135 135 assert_equal orig.subject, issue.subject
136 136 assert_equal orig.tracker, issue.tracker
137 137 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
138 138 end
139 139
140 140 def test_should_close_duplicates
141 141 # Create 3 issues
142 142 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
143 143 assert issue1.save
144 144 issue2 = issue1.clone
145 145 assert issue2.save
146 146 issue3 = issue1.clone
147 147 assert issue3.save
148 148
149 149 # 2 is a dupe of 1
150 150 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
151 151 # And 3 is a dupe of 2
152 152 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
153 153 # And 3 is a dupe of 1 (circular duplicates)
154 154 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
155 155
156 156 assert issue1.reload.duplicates.include?(issue2)
157 157
158 158 # Closing issue 1
159 159 issue1.init_journal(User.find(:first), "Closing issue1")
160 160 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
161 161 assert issue1.save
162 162 # 2 and 3 should be also closed
163 163 assert issue2.reload.closed?
164 164 assert issue3.reload.closed?
165 165 end
166 166
167 167 def test_should_not_close_duplicated_issue
168 168 # Create 3 issues
169 169 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
170 170 assert issue1.save
171 171 issue2 = issue1.clone
172 172 assert issue2.save
173 173
174 174 # 2 is a dupe of 1
175 175 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
176 176 # 2 is a dup of 1 but 1 is not a duplicate of 2
177 177 assert !issue2.reload.duplicates.include?(issue1)
178 178
179 179 # Closing issue 2
180 180 issue2.init_journal(User.find(:first), "Closing issue2")
181 181 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
182 182 assert issue2.save
183 183 # 1 should not be also closed
184 184 assert !issue1.reload.closed?
185 185 end
186 186
187 187 def test_move_to_another_project_with_same_category
188 188 issue = Issue.find(1)
189 189 assert issue.move_to(Project.find(2))
190 190 issue.reload
191 191 assert_equal 2, issue.project_id
192 192 # Category changes
193 193 assert_equal 4, issue.category_id
194 194 # Make sure time entries were move to the target project
195 195 assert_equal 2, issue.time_entries.first.project_id
196 196 end
197 197
198 198 def test_move_to_another_project_without_same_category
199 199 issue = Issue.find(2)
200 200 assert issue.move_to(Project.find(2))
201 201 issue.reload
202 202 assert_equal 2, issue.project_id
203 203 # Category cleared
204 204 assert_nil issue.category_id
205 205 end
206 206
207 207 def test_copy_to_the_same_project
208 208 issue = Issue.find(1)
209 209 copy = nil
210 210 assert_difference 'Issue.count' do
211 211 copy = issue.move_to(issue.project, nil, :copy => true)
212 212 end
213 213 assert_kind_of Issue, copy
214 214 assert_equal issue.project, copy.project
215 215 assert_equal "125", copy.custom_value_for(2).value
216 216 end
217 217
218 218 def test_copy_to_another_project_and_tracker
219 219 issue = Issue.find(1)
220 220 copy = nil
221 221 assert_difference 'Issue.count' do
222 222 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
223 223 end
224 224 assert_kind_of Issue, copy
225 225 assert_equal Project.find(3), copy.project
226 226 assert_equal Tracker.find(2), copy.tracker
227 227 # Custom field #2 is not associated with target tracker
228 228 assert_nil copy.custom_value_for(2)
229 229 end
230 230
231 231 def test_issue_destroy
232 232 Issue.find(1).destroy
233 233 assert_nil Issue.find_by_id(1)
234 234 assert_nil TimeEntry.find_by_issue_id(1)
235 235 end
236 236
237 237 def test_overdue
238 238 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
239 239 assert !Issue.new(:due_date => Date.today).overdue?
240 240 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
241 241 assert !Issue.new(:due_date => nil).overdue?
242 242 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
243 243 end
244
245 def test_create_should_send_email_notification
246 ActionMailer::Base.deliveries.clear
247 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create', :estimated_hours => '1:30')
248
249 assert issue.save
250 assert_equal 1, ActionMailer::Base.deliveries.size
251 end
244 252 end
@@ -1,39 +1,50
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 JournalTest < Test::Unit::TestCase
21 21 fixtures :issues, :issue_statuses, :journals, :journal_details
22 22
23 23 def setup
24 24 @journal = Journal.find 1
25 25 end
26 26
27 27 def test_journalized_is_an_issue
28 28 issue = @journal.issue
29 29 assert_kind_of Issue, issue
30 30 assert_equal 1, issue.id
31 31 end
32 32
33 33 def test_new_status
34 34 status = @journal.new_status
35 35 assert_not_nil status
36 36 assert_kind_of IssueStatus, status
37 37 assert_equal 2, status.id
38 38 end
39
40 def test_create_should_send_email_notification
41 ActionMailer::Base.deliveries.clear
42 issue = Issue.find(:first)
43 user = User.find(:first)
44 journal = issue.init_journal(user, issue)
45
46 assert journal.save
47 assert_equal 1, ActionMailer::Base.deliveries.size
48 end
49
39 50 end
@@ -1,190 +1,205
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 MailHandlerTest < Test::Unit::TestCase
21 21 fixtures :users, :projects,
22 22 :enabled_modules,
23 23 :roles,
24 24 :members,
25 25 :issues,
26 26 :issue_statuses,
27 27 :workflows,
28 28 :trackers,
29 29 :projects_trackers,
30 30 :enumerations,
31 31 :issue_categories,
32 32 :custom_fields,
33 33 :custom_fields_trackers,
34 34 :boards,
35 35 :messages
36 36
37 37 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
38 38
39 39 def setup
40 40 ActionMailer::Base.deliveries.clear
41 41 end
42 42
43 43 def test_add_issue
44 44 # This email contains: 'Project: onlinestore'
45 45 issue = submit_email('ticket_on_given_project.eml')
46 46 assert issue.is_a?(Issue)
47 47 assert !issue.new_record?
48 48 issue.reload
49 49 assert_equal 'New ticket on a given project', issue.subject
50 50 assert_equal User.find_by_login('jsmith'), issue.author
51 51 assert_equal Project.find(2), issue.project
52 52 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
53 53 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
54 54 # keywords should be removed from the email body
55 55 assert !issue.description.match(/^Project:/i)
56 56 assert !issue.description.match(/^Status:/i)
57 57 end
58 58
59 59 def test_add_issue_with_status
60 60 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
61 61 issue = submit_email('ticket_on_given_project.eml')
62 62 assert issue.is_a?(Issue)
63 63 assert !issue.new_record?
64 64 issue.reload
65 65 assert_equal Project.find(2), issue.project
66 66 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
67 67 end
68 68
69 69 def test_add_issue_with_attributes_override
70 70 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
71 71 assert issue.is_a?(Issue)
72 72 assert !issue.new_record?
73 73 issue.reload
74 74 assert_equal 'New ticket on a given project', issue.subject
75 75 assert_equal User.find_by_login('jsmith'), issue.author
76 76 assert_equal Project.find(2), issue.project
77 77 assert_equal 'Feature request', issue.tracker.to_s
78 78 assert_equal 'Stock management', issue.category.to_s
79 79 assert_equal 'Urgent', issue.priority.to_s
80 80 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
81 81 end
82 82
83 83 def test_add_issue_with_partial_attributes_override
84 84 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
85 85 assert issue.is_a?(Issue)
86 86 assert !issue.new_record?
87 87 issue.reload
88 88 assert_equal 'New ticket on a given project', issue.subject
89 89 assert_equal User.find_by_login('jsmith'), issue.author
90 90 assert_equal Project.find(2), issue.project
91 91 assert_equal 'Feature request', issue.tracker.to_s
92 92 assert_nil issue.category
93 93 assert_equal 'High', issue.priority.to_s
94 94 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
95 95 end
96 96
97 97 def test_add_issue_with_attachment_to_specific_project
98 98 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
99 99 assert issue.is_a?(Issue)
100 100 assert !issue.new_record?
101 101 issue.reload
102 102 assert_equal 'Ticket created by email with attachment', issue.subject
103 103 assert_equal User.find_by_login('jsmith'), issue.author
104 104 assert_equal Project.find(2), issue.project
105 105 assert_equal 'This is a new ticket with attachments', issue.description
106 106 # Attachment properties
107 107 assert_equal 1, issue.attachments.size
108 108 assert_equal 'Paella.jpg', issue.attachments.first.filename
109 109 assert_equal 'image/jpeg', issue.attachments.first.content_type
110 110 assert_equal 10790, issue.attachments.first.filesize
111 111 end
112 112
113 113 def test_add_issue_with_custom_fields
114 114 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
115 115 assert issue.is_a?(Issue)
116 116 assert !issue.new_record?
117 117 issue.reload
118 118 assert_equal 'New ticket with custom field values', issue.subject
119 119 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
120 120 assert !issue.description.match(/^searchable field:/i)
121 121 end
122 122
123 123 def test_add_issue_with_cc
124 124 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
125 125 assert issue.is_a?(Issue)
126 126 assert !issue.new_record?
127 127 issue.reload
128 128 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
129 129 assert_equal 1, issue.watchers.size
130 130 end
131 131
132 132 def test_add_issue_without_from_header
133 133 Role.anonymous.add_permission!(:add_issues)
134 134 assert_equal false, submit_email('ticket_without_from_header.eml')
135 135 end
136
137 def test_add_issue_should_send_email_notification
138 ActionMailer::Base.deliveries.clear
139 # This email contains: 'Project: onlinestore'
140 issue = submit_email('ticket_on_given_project.eml')
141 assert issue.is_a?(Issue)
142 assert_equal 1, ActionMailer::Base.deliveries.size
143 end
136 144
137 145 def test_add_issue_note
138 146 journal = submit_email('ticket_reply.eml')
139 147 assert journal.is_a?(Journal)
140 148 assert_equal User.find_by_login('jsmith'), journal.user
141 149 assert_equal Issue.find(2), journal.journalized
142 150 assert_match /This is reply/, journal.notes
143 151 end
144 152
145 153 def test_add_issue_note_with_status_change
146 154 # This email contains: 'Status: Resolved'
147 155 journal = submit_email('ticket_reply_with_status.eml')
148 156 assert journal.is_a?(Journal)
149 157 issue = Issue.find(journal.issue.id)
150 158 assert_equal User.find_by_login('jsmith'), journal.user
151 159 assert_equal Issue.find(2), journal.journalized
152 160 assert_match /This is reply/, journal.notes
153 161 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
154 162 end
163
164 def test_add_issue_note_should_send_email_notification
165 ActionMailer::Base.deliveries.clear
166 journal = submit_email('ticket_reply.eml')
167 assert journal.is_a?(Journal)
168 assert_equal 1, ActionMailer::Base.deliveries.size
169 end
155 170
156 171 def test_reply_to_a_message
157 172 m = submit_email('message_reply.eml')
158 173 assert m.is_a?(Message)
159 174 assert !m.new_record?
160 175 m.reload
161 176 assert_equal 'Reply via email', m.subject
162 177 # The email replies to message #2 which is part of the thread of message #1
163 178 assert_equal Message.find(1), m.parent
164 179 end
165 180
166 181 def test_reply_to_a_message_by_subject
167 182 m = submit_email('message_reply_by_subject.eml')
168 183 assert m.is_a?(Message)
169 184 assert !m.new_record?
170 185 m.reload
171 186 assert_equal 'Reply to the first post', m.subject
172 187 assert_equal Message.find(1), m.parent
173 188 end
174 189
175 190 def test_should_strip_tags_of_html_only_emails
176 191 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
177 192 assert issue.is_a?(Issue)
178 193 assert !issue.new_record?
179 194 issue.reload
180 195 assert_equal 'HTML email', issue.subject
181 196 assert_equal 'This is a html-only email.', issue.description
182 197 end
183 198
184 199 private
185 200
186 201 def submit_email(filename, options={})
187 202 raw = IO.read(File.join(FIXTURES_PATH, filename))
188 203 MailHandler.receive(raw, options)
189 204 end
190 205 end
@@ -1,63 +1,77
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 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class NewsTest < Test::Unit::TestCase
21 21 fixtures :projects, :users, :roles, :members, :enabled_modules, :news
22 22
23 def valid_news
24 { :title => 'Test news', :description => 'Lorem ipsum etc', :author => User.find(:first) }
25 end
26
27
23 28 def setup
24 29 end
25 30
31 def test_create_should_send_email_notification
32 ActionMailer::Base.deliveries.clear
33 Setting.notified_events << 'news_added'
34 news = Project.find(:first).news.new(valid_news)
35
36 assert news.save
37 assert_equal 1, ActionMailer::Base.deliveries.size
38 end
39
26 40 def test_should_include_news_for_projects_with_news_enabled
27 41 project = projects(:projects_001)
28 42 assert project.enabled_modules.any?{ |em| em.name == 'news' }
29 43
30 44 # News.latest should return news from projects_001
31 45 assert News.latest.any? { |news| news.project == project }
32 46 end
33 47
34 48 def test_should_not_include_news_for_projects_with_news_disabled
35 49 # The projects_002 (OnlineStore) doesn't have the news module enabled, use that project for this test
36 50 project = projects(:projects_002)
37 51 assert ! project.enabled_modules.any?{ |em| em.name == 'news' }
38 52
39 53 # Add a piece of news to the project
40 news = project.news.create(:title => 'Test news', :description => 'This should not be returned by News.latest')
54 news = project.news.create(valid_news)
41 55
42 56 # News.latest should not return that new piece of news
43 57 assert News.latest.include?(news) == false
44 58 end
45 59
46 60 def test_should_only_include_news_from_projects_visibly_to_the_user
47 61 # users_001 has no memberships so can only get news from public project
48 62 assert News.latest(users(:users_001)).all? { |news| news.project.is_public? }
49 63 end
50 64
51 65 def test_should_limit_the_amount_of_returned_news
52 66 # Make sure we have a bunch of news stories
53 10.times { projects(:projects_001).news.create(:title => 'Test news', :description => 'Lorem ipsum etc') }
67 10.times { projects(:projects_001).news.create(valid_news) }
54 68 assert_equal 2, News.latest(users(:users_002), 2).size
55 69 assert_equal 6, News.latest(users(:users_002), 6).size
56 70 end
57 71
58 72 def test_should_return_5_news_stories_by_default
59 73 # Make sure we have a bunch of news stories
60 10.times { projects(:projects_001).news.create(:title => 'Test news', :description => 'Lorem ipsum etc') }
74 10.times { projects(:projects_001).news.create(valid_news) }
61 75 assert_equal 5, News.latest(users(:users_004)).size
62 76 end
63 77 end
General Comments 0
You need to be logged in to leave comments. Login now