##// END OF EJS Templates
Moved some permission checks for issue update from controller to model....
Jean-Philippe Lang -
r4279:0eb7d8f6149c
parent child
Show More
@@ -1,332 +1,321
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, :create]
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :update]
23 23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 25 before_filter :find_project, :only => [:new, :create]
26 26 before_filter :authorize, :except => [:index]
27 27 before_filter :find_optional_project, :only => [:index]
28 28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 30 accept_key_auth :index, :show, :create, :update, :destroy
31 31
32 32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 33
34 34 helper :journals
35 35 helper :projects
36 36 include ProjectsHelper
37 37 helper :custom_fields
38 38 include CustomFieldsHelper
39 39 helper :issue_relations
40 40 include IssueRelationsHelper
41 41 helper :watchers
42 42 include WatchersHelper
43 43 helper :attachments
44 44 include AttachmentsHelper
45 45 helper :queries
46 46 include QueriesHelper
47 47 helper :sort
48 48 include SortHelper
49 49 include IssuesHelper
50 50 helper :timelog
51 51 helper :gantt
52 52 include Redmine::Export::PDF
53 53
54 54 verify :method => [:post, :delete],
55 55 :only => :destroy,
56 56 :render => { :nothing => true, :status => :method_not_allowed }
57 57
58 58 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
59 59 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
60 60 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
61 61
62 62 def index
63 63 retrieve_query
64 64 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
65 65 sort_update(@query.sortable_columns)
66 66
67 67 if @query.valid?
68 68 limit = case params[:format]
69 69 when 'csv', 'pdf'
70 70 Setting.issues_export_limit.to_i
71 71 when 'atom'
72 72 Setting.feeds_limit.to_i
73 73 else
74 74 per_page_option
75 75 end
76 76
77 77 @issue_count = @query.issue_count
78 78 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
79 79 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
80 80 :order => sort_clause,
81 81 :offset => @issue_pages.current.offset,
82 82 :limit => limit)
83 83 @issue_count_by_group = @query.issue_count_by_group
84 84
85 85 respond_to do |format|
86 86 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
87 87 format.xml { render :layout => false }
88 88 format.json { render :text => @issues.to_json, :layout => false }
89 89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
90 90 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
91 91 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
92 92 end
93 93 else
94 94 # Send html if the query is not valid
95 95 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
96 96 end
97 97 rescue ActiveRecord::RecordNotFound
98 98 render_404
99 99 end
100 100
101 101 def show
102 102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
103 103 @journals.each_with_index {|j,i| j.indice = i+1}
104 104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
105 105 @changesets = @issue.changesets.visible.all
106 106 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
107 107 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
108 108 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
109 109 @priorities = IssuePriority.all
110 110 @time_entry = TimeEntry.new
111 111 respond_to do |format|
112 112 format.html { render :template => 'issues/show.rhtml' }
113 113 format.xml { render :layout => false }
114 114 format.json { render :text => @issue.to_json, :layout => false }
115 115 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
116 116 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
117 117 end
118 118 end
119 119
120 120 # Add a new issue
121 121 # The new issue will be created from an existing one if copy_from parameter is given
122 122 def new
123 123 respond_to do |format|
124 124 format.html { render :action => 'new', :layout => !request.xhr? }
125 125 format.js { render :partial => 'attributes' }
126 126 end
127 127 end
128 128
129 129 def create
130 130 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
131 131 if @issue.save
132 132 attachments = Attachment.attach_files(@issue, params[:attachments])
133 133 render_attachment_warning_if_needed(@issue)
134 134 flash[:notice] = l(:notice_successful_create)
135 135 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
136 136 respond_to do |format|
137 137 format.html {
138 138 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
139 139 { :action => 'show', :id => @issue })
140 140 }
141 141 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
142 142 format.json { render :text => @issue.to_json, :status => :created, :location => url_for(:controller => 'issues', :action => 'show'), :layout => false }
143 143 end
144 144 return
145 145 else
146 146 respond_to do |format|
147 147 format.html { render :action => 'new' }
148 148 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
149 149 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
150 150 end
151 151 end
152 152 end
153 153
154 # Attributes that can be updated on workflow transition (without :edit permission)
155 # TODO: make it configurable (at least per role)
156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
157
158 154 def edit
159 155 update_issue_from_params
160 156
161 157 @journal = @issue.current_journal
162 158
163 159 respond_to do |format|
164 160 format.html { }
165 161 format.xml { }
166 162 end
167 163 end
168 164
169 165 def update
170 166 update_issue_from_params
171 167
172 168 if @issue.save_issue_with_child_records(params, @time_entry)
173 169 render_attachment_warning_if_needed(@issue)
174 170 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
175 171
176 172 respond_to do |format|
177 173 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
178 174 format.xml { head :ok }
179 175 format.json { head :ok }
180 176 end
181 177 else
182 178 render_attachment_warning_if_needed(@issue)
183 179 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
184 180 @journal = @issue.current_journal
185 181
186 182 respond_to do |format|
187 183 format.html { render :action => 'edit' }
188 184 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
189 185 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
190 186 end
191 187 end
192 188 end
193 189
194 190 # Bulk edit a set of issues
195 191 def bulk_edit
196 192 @issues.sort!
197 193 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
198 194 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
199 195 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
200 196 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
201 197 end
202 198
203 199 def bulk_update
204 200 @issues.sort!
205 201 attributes = parse_params_for_bulk_issue_attributes(params)
206 202
207 203 unsaved_issue_ids = []
208 204 @issues.each do |issue|
209 205 issue.reload
210 206 journal = issue.init_journal(User.current, params[:notes])
211 207 issue.safe_attributes = attributes
212 208 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
213 209 unless issue.save
214 210 # Keep unsaved issue ids to display them in flash error
215 211 unsaved_issue_ids << issue.id
216 212 end
217 213 end
218 214 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
219 215 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
220 216 end
221 217
222 218 def destroy
223 219 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
224 220 if @hours > 0
225 221 case params[:todo]
226 222 when 'destroy'
227 223 # nothing to do
228 224 when 'nullify'
229 225 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
230 226 when 'reassign'
231 227 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
232 228 if reassign_to.nil?
233 229 flash.now[:error] = l(:error_issue_not_found_in_project)
234 230 return
235 231 else
236 232 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
237 233 end
238 234 else
239 235 unless params[:format] == 'xml' || params[:format] == 'json'
240 236 # display the destroy form if it's a user request
241 237 return
242 238 end
243 239 end
244 240 end
245 241 @issues.each(&:destroy)
246 242 respond_to do |format|
247 243 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
248 244 format.xml { head :ok }
249 245 format.json { head :ok }
250 246 end
251 247 end
252 248
253 249 private
254 250 def find_issue
255 251 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
256 252 @project = @issue.project
257 253 rescue ActiveRecord::RecordNotFound
258 254 render_404
259 255 end
260 256
261 257 def find_project
262 258 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
263 259 @project = Project.find(project_id)
264 260 rescue ActiveRecord::RecordNotFound
265 261 render_404
266 262 end
267 263
268 264 # Used by #edit and #update to set some common instance variables
269 265 # from the params
270 266 # TODO: Refactor, not everything in here is needed by #edit
271 267 def update_issue_from_params
272 268 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
273 269 @priorities = IssuePriority.all
274 270 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
275 271 @time_entry = TimeEntry.new
276 272
277 273 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
278 274 @issue.init_journal(User.current, @notes)
279 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
280 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
281 attrs = params[:issue].dup
282 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
283 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
284 @issue.safe_attributes = attrs
285 end
286
275 @issue.safe_attributes = params[:issue]
287 276 end
288 277
289 278 # TODO: Refactor, lots of extra code in here
290 279 # TODO: Changing tracker on an existing issue should not trigger this
291 280 def build_new_issue_from_params
292 281 if params[:id].blank?
293 282 @issue = Issue.new
294 283 @issue.copy_from(params[:copy_from]) if params[:copy_from]
295 284 @issue.project = @project
296 285 else
297 286 @issue = @project.issues.visible.find(params[:id])
298 287 end
299 288
300 289 @issue.project = @project
301 290 # Tracker must be set before custom field values
302 291 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
303 292 if @issue.tracker.nil?
304 293 render_error l(:error_no_tracker_in_project)
305 294 return false
306 295 end
307 296 @issue.start_date ||= Date.today
308 297 if params[:issue].is_a?(Hash)
309 298 @issue.safe_attributes = params[:issue]
310 299 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
311 300 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
312 301 end
313 302 end
314 303 @issue.author = User.current
315 304 @priorities = IssuePriority.all
316 305 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
317 306 end
318 307
319 308 def check_for_default_issue_status
320 309 if IssueStatus.default.nil?
321 310 render_error l(:error_no_default_issue_status)
322 311 return false
323 312 end
324 313 end
325 314
326 315 def parse_params_for_bulk_issue_attributes(params)
327 316 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
328 317 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
329 318 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
330 319 attributes
331 320 end
332 321 end
@@ -1,866 +1,884
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 Issue < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :tracker
21 21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27 27
28 28 has_many :journals, :as => :journalized, :dependent => :destroy
29 29 has_many :time_entries, :dependent => :delete_all
30 30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31 31
32 32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34 34
35 35 acts_as_nested_set :scope => 'root_id'
36 36 acts_as_attachable :after_remove => :attachment_removed
37 37 acts_as_customizable
38 38 acts_as_watchable
39 39 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
40 40 :include => [:project, :journals],
41 41 # sort by id so that limited eager loading doesn't break with postgresql
42 42 :order_column => "#{table_name}.id"
43 43 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
44 44 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
45 45 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
46 46
47 47 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
48 48 :author_key => :author_id
49 49
50 50 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
51 51
52 52 attr_reader :current_journal
53 53
54 54 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
55 55
56 56 validates_length_of :subject, :maximum => 255
57 57 validates_inclusion_of :done_ratio, :in => 0..100
58 58 validates_numericality_of :estimated_hours, :allow_nil => true
59 59
60 60 named_scope :visible, lambda {|*args| { :include => :project,
61 61 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
62 62
63 63 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
64 64
65 65 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
66 66 named_scope :with_limit, lambda { |limit| { :limit => limit} }
67 67 named_scope :on_active_project, :include => [:status, :project, :tracker],
68 68 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
69 69 named_scope :for_gantt, lambda {
70 70 {
71 71 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
72 72 :order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC"
73 73 }
74 74 }
75 75
76 76 named_scope :without_version, lambda {
77 77 {
78 78 :conditions => { :fixed_version_id => nil}
79 79 }
80 80 }
81 81
82 82 named_scope :with_query, lambda {|query|
83 83 {
84 84 :conditions => Query.merge_conditions(query.statement)
85 85 }
86 86 }
87 87
88 88 before_create :default_assign
89 89 before_save :close_duplicates, :update_done_ratio_from_issue_status
90 90 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
91 91 after_destroy :destroy_children
92 92 after_destroy :update_parent_attributes
93 93
94 94 # Returns true if usr or current user is allowed to view the issue
95 95 def visible?(usr=nil)
96 96 (usr || User.current).allowed_to?(:view_issues, self.project)
97 97 end
98 98
99 99 def after_initialize
100 100 if new_record?
101 101 # set default values for new records only
102 102 self.status ||= IssueStatus.default
103 103 self.priority ||= IssuePriority.default
104 104 end
105 105 end
106 106
107 107 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
108 108 def available_custom_fields
109 109 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
110 110 end
111 111
112 112 def copy_from(arg)
113 113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
114 114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
115 115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
116 116 self.status = issue.status
117 117 self
118 118 end
119 119
120 120 # Moves/copies an issue to a new project and tracker
121 121 # Returns the moved/copied issue on success, false on failure
122 122 def move_to_project(*args)
123 123 ret = Issue.transaction do
124 124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
125 125 end || false
126 126 end
127 127
128 128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
129 129 options ||= {}
130 130 issue = options[:copy] ? self.class.new.copy_from(self) : self
131 131
132 132 if new_project && issue.project_id != new_project.id
133 133 # delete issue relations
134 134 unless Setting.cross_project_issue_relations?
135 135 issue.relations_from.clear
136 136 issue.relations_to.clear
137 137 end
138 138 # issue is moved to another project
139 139 # reassign to the category with same name if any
140 140 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
141 141 issue.category = new_category
142 142 # Keep the fixed_version if it's still valid in the new_project
143 143 unless new_project.shared_versions.include?(issue.fixed_version)
144 144 issue.fixed_version = nil
145 145 end
146 146 issue.project = new_project
147 147 if issue.parent && issue.parent.project_id != issue.project_id
148 148 issue.parent_issue_id = nil
149 149 end
150 150 end
151 151 if new_tracker
152 152 issue.tracker = new_tracker
153 153 issue.reset_custom_values!
154 154 end
155 155 if options[:copy]
156 156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
157 157 issue.status = if options[:attributes] && options[:attributes][:status_id]
158 158 IssueStatus.find_by_id(options[:attributes][:status_id])
159 159 else
160 160 self.status
161 161 end
162 162 end
163 163 # Allow bulk setting of attributes on the issue
164 164 if options[:attributes]
165 165 issue.attributes = options[:attributes]
166 166 end
167 167 if issue.save
168 168 unless options[:copy]
169 169 # Manually update project_id on related time entries
170 170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
171 171
172 172 issue.children.each do |child|
173 173 unless child.move_to_project_without_transaction(new_project)
174 174 # Move failed and transaction was rollback'd
175 175 return false
176 176 end
177 177 end
178 178 end
179 179 else
180 180 return false
181 181 end
182 182 issue
183 183 end
184 184
185 185 def status_id=(sid)
186 186 self.status = nil
187 187 write_attribute(:status_id, sid)
188 188 end
189 189
190 190 def priority_id=(pid)
191 191 self.priority = nil
192 192 write_attribute(:priority_id, pid)
193 193 end
194 194
195 195 def tracker_id=(tid)
196 196 self.tracker = nil
197 197 result = write_attribute(:tracker_id, tid)
198 198 @custom_field_values = nil
199 199 result
200 200 end
201 201
202 202 # Overrides attributes= so that tracker_id gets assigned first
203 203 def attributes_with_tracker_first=(new_attributes, *args)
204 204 return if new_attributes.nil?
205 205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
206 206 if new_tracker_id
207 207 self.tracker_id = new_tracker_id
208 208 end
209 209 send :attributes_without_tracker_first=, new_attributes, *args
210 210 end
211 211 # Do not redefine alias chain on reload (see #4838)
212 212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
213 213
214 214 def estimated_hours=(h)
215 215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
216 216 end
217 217
218 218 SAFE_ATTRIBUTES = %w(
219 219 tracker_id
220 220 status_id
221 221 parent_issue_id
222 222 category_id
223 223 assigned_to_id
224 224 priority_id
225 225 fixed_version_id
226 226 subject
227 227 description
228 228 start_date
229 229 due_date
230 230 done_ratio
231 231 estimated_hours
232 232 custom_field_values
233 233 lock_version
234 234 ) unless const_defined?(:SAFE_ATTRIBUTES)
235 235
236 SAFE_ATTRIBUTES_ON_TRANSITION = %w(
237 status_id
238 assigned_to_id
239 fixed_version_id
240 done_ratio
241 ) unless const_defined?(:SAFE_ATTRIBUTES_ON_TRANSITION)
242
236 243 # Safely sets attributes
237 244 # Should be called from controllers instead of #attributes=
238 245 # attr_accessible is too rough because we still want things like
239 246 # Issue.new(:project => foo) to work
240 247 # TODO: move workflow/permission checks from controllers to here
241 248 def safe_attributes=(attrs, user=User.current)
242 return if attrs.nil?
249 return unless attrs.is_a?(Hash)
250
251 new_statuses_allowed = new_statuses_allowed_to(user)
252
253 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
254 if new_record? || user.allowed_to?(:edit_issues, project)
243 255 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
256 elsif new_statuses_allowed.any?
257 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES_ON_TRANSITION.include?(k)}
258 else
259 return
260 end
261
244 262 if attrs['status_id']
245 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
263 unless new_statuses_allowed.collect(&:id).include?(attrs['status_id'].to_i)
246 264 attrs.delete('status_id')
247 265 end
248 266 end
249 267
250 268 unless leaf?
251 269 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
252 270 end
253 271
254 272 if attrs.has_key?('parent_issue_id')
255 273 if !user.allowed_to?(:manage_subtasks, project)
256 274 attrs.delete('parent_issue_id')
257 275 elsif !attrs['parent_issue_id'].blank?
258 276 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
259 277 end
260 278 end
261 279
262 280 self.attributes = attrs
263 281 end
264 282
265 283 def done_ratio
266 284 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
267 285 status.default_done_ratio
268 286 else
269 287 read_attribute(:done_ratio)
270 288 end
271 289 end
272 290
273 291 def self.use_status_for_done_ratio?
274 292 Setting.issue_done_ratio == 'issue_status'
275 293 end
276 294
277 295 def self.use_field_for_done_ratio?
278 296 Setting.issue_done_ratio == 'issue_field'
279 297 end
280 298
281 299 def validate
282 300 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
283 301 errors.add :due_date, :not_a_date
284 302 end
285 303
286 304 if self.due_date and self.start_date and self.due_date < self.start_date
287 305 errors.add :due_date, :greater_than_start_date
288 306 end
289 307
290 308 if start_date && soonest_start && start_date < soonest_start
291 309 errors.add :start_date, :invalid
292 310 end
293 311
294 312 if fixed_version
295 313 if !assignable_versions.include?(fixed_version)
296 314 errors.add :fixed_version_id, :inclusion
297 315 elsif reopened? && fixed_version.closed?
298 316 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
299 317 end
300 318 end
301 319
302 320 # Checks that the issue can not be added/moved to a disabled tracker
303 321 if project && (tracker_id_changed? || project_id_changed?)
304 322 unless project.trackers.include?(tracker)
305 323 errors.add :tracker_id, :inclusion
306 324 end
307 325 end
308 326
309 327 # Checks parent issue assignment
310 328 if @parent_issue
311 329 if @parent_issue.project_id != project_id
312 330 errors.add :parent_issue_id, :not_same_project
313 331 elsif !new_record?
314 332 # moving an existing issue
315 333 if @parent_issue.root_id != root_id
316 334 # we can always move to another tree
317 335 elsif move_possible?(@parent_issue)
318 336 # move accepted inside tree
319 337 else
320 338 errors.add :parent_issue_id, :not_a_valid_parent
321 339 end
322 340 end
323 341 end
324 342 end
325 343
326 344 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
327 345 # even if the user turns off the setting later
328 346 def update_done_ratio_from_issue_status
329 347 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
330 348 self.done_ratio = status.default_done_ratio
331 349 end
332 350 end
333 351
334 352 def init_journal(user, notes = "")
335 353 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
336 354 @issue_before_change = self.clone
337 355 @issue_before_change.status = self.status
338 356 @custom_values_before_change = {}
339 357 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
340 358 # Make sure updated_on is updated when adding a note.
341 359 updated_on_will_change!
342 360 @current_journal
343 361 end
344 362
345 363 # Return true if the issue is closed, otherwise false
346 364 def closed?
347 365 self.status.is_closed?
348 366 end
349 367
350 368 # Return true if the issue is being reopened
351 369 def reopened?
352 370 if !new_record? && status_id_changed?
353 371 status_was = IssueStatus.find_by_id(status_id_was)
354 372 status_new = IssueStatus.find_by_id(status_id)
355 373 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
356 374 return true
357 375 end
358 376 end
359 377 false
360 378 end
361 379
362 380 # Return true if the issue is being closed
363 381 def closing?
364 382 if !new_record? && status_id_changed?
365 383 status_was = IssueStatus.find_by_id(status_id_was)
366 384 status_new = IssueStatus.find_by_id(status_id)
367 385 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
368 386 return true
369 387 end
370 388 end
371 389 false
372 390 end
373 391
374 392 # Returns true if the issue is overdue
375 393 def overdue?
376 394 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
377 395 end
378 396
379 397 # Is the amount of work done less than it should for the due date
380 398 def behind_schedule?
381 399 return false if start_date.nil? || due_date.nil?
382 400 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
383 401 return done_date <= Date.today
384 402 end
385 403
386 404 # Does this issue have children?
387 405 def children?
388 406 !leaf?
389 407 end
390 408
391 409 # Users the issue can be assigned to
392 410 def assignable_users
393 411 users = project.assignable_users
394 412 users << author if author
395 413 users.uniq.sort
396 414 end
397 415
398 416 # Versions that the issue can be assigned to
399 417 def assignable_versions
400 418 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
401 419 end
402 420
403 421 # Returns true if this issue is blocked by another issue that is still open
404 422 def blocked?
405 423 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
406 424 end
407 425
408 426 # Returns an array of status that user is able to apply
409 427 def new_statuses_allowed_to(user, include_default=false)
410 428 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
411 429 statuses << status unless statuses.empty?
412 430 statuses << IssueStatus.default if include_default
413 431 statuses = statuses.uniq.sort
414 432 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
415 433 end
416 434
417 435 # Returns the mail adresses of users that should be notified
418 436 def recipients
419 437 notified = project.notified_users
420 438 # Author and assignee are always notified unless they have been
421 439 # locked or don't want to be notified
422 440 notified << author if author && author.active? && author.notify_about?(self)
423 441 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
424 442 notified.uniq!
425 443 # Remove users that can not view the issue
426 444 notified.reject! {|user| !visible?(user)}
427 445 notified.collect(&:mail)
428 446 end
429 447
430 448 # Returns the total number of hours spent on this issue and its descendants
431 449 #
432 450 # Example:
433 451 # spent_hours => 0.0
434 452 # spent_hours => 50.2
435 453 def spent_hours
436 454 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
437 455 end
438 456
439 457 def relations
440 458 (relations_from + relations_to).sort
441 459 end
442 460
443 461 def all_dependent_issues
444 462 dependencies = []
445 463 relations_from.each do |relation|
446 464 dependencies << relation.issue_to
447 465 dependencies += relation.issue_to.all_dependent_issues
448 466 end
449 467 dependencies
450 468 end
451 469
452 470 # Returns an array of issues that duplicate this one
453 471 def duplicates
454 472 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
455 473 end
456 474
457 475 # Returns the due date or the target due date if any
458 476 # Used on gantt chart
459 477 def due_before
460 478 due_date || (fixed_version ? fixed_version.effective_date : nil)
461 479 end
462 480
463 481 # Returns the time scheduled for this issue.
464 482 #
465 483 # Example:
466 484 # Start Date: 2/26/09, End Date: 3/04/09
467 485 # duration => 6
468 486 def duration
469 487 (start_date && due_date) ? due_date - start_date : 0
470 488 end
471 489
472 490 def soonest_start
473 491 @soonest_start ||= (
474 492 relations_to.collect{|relation| relation.successor_soonest_start} +
475 493 ancestors.collect(&:soonest_start)
476 494 ).compact.max
477 495 end
478 496
479 497 def reschedule_after(date)
480 498 return if date.nil?
481 499 if leaf?
482 500 if start_date.nil? || start_date < date
483 501 self.start_date, self.due_date = date, date + duration
484 502 save
485 503 end
486 504 else
487 505 leaves.each do |leaf|
488 506 leaf.reschedule_after(date)
489 507 end
490 508 end
491 509 end
492 510
493 511 def <=>(issue)
494 512 if issue.nil?
495 513 -1
496 514 elsif root_id != issue.root_id
497 515 (root_id || 0) <=> (issue.root_id || 0)
498 516 else
499 517 (lft || 0) <=> (issue.lft || 0)
500 518 end
501 519 end
502 520
503 521 def to_s
504 522 "#{tracker} ##{id}: #{subject}"
505 523 end
506 524
507 525 # Returns a string of css classes that apply to the issue
508 526 def css_classes
509 527 s = "issue status-#{status.position} priority-#{priority.position}"
510 528 s << ' closed' if closed?
511 529 s << ' overdue' if overdue?
512 530 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
513 531 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
514 532 s
515 533 end
516 534
517 535 # Saves an issue, time_entry, attachments, and a journal from the parameters
518 536 # Returns false if save fails
519 537 def save_issue_with_child_records(params, existing_time_entry=nil)
520 538 Issue.transaction do
521 539 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
522 540 @time_entry = existing_time_entry || TimeEntry.new
523 541 @time_entry.project = project
524 542 @time_entry.issue = self
525 543 @time_entry.user = User.current
526 544 @time_entry.spent_on = Date.today
527 545 @time_entry.attributes = params[:time_entry]
528 546 self.time_entries << @time_entry
529 547 end
530 548
531 549 if valid?
532 550 attachments = Attachment.attach_files(self, params[:attachments])
533 551
534 552 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
535 553 # TODO: Rename hook
536 554 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
537 555 begin
538 556 if save
539 557 # TODO: Rename hook
540 558 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
541 559 else
542 560 raise ActiveRecord::Rollback
543 561 end
544 562 rescue ActiveRecord::StaleObjectError
545 563 attachments[:files].each(&:destroy)
546 564 errors.add_to_base l(:notice_locking_conflict)
547 565 raise ActiveRecord::Rollback
548 566 end
549 567 end
550 568 end
551 569 end
552 570
553 571 # Unassigns issues from +version+ if it's no longer shared with issue's project
554 572 def self.update_versions_from_sharing_change(version)
555 573 # Update issues assigned to the version
556 574 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
557 575 end
558 576
559 577 # Unassigns issues from versions that are no longer shared
560 578 # after +project+ was moved
561 579 def self.update_versions_from_hierarchy_change(project)
562 580 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
563 581 # Update issues of the moved projects and issues assigned to a version of a moved project
564 582 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
565 583 end
566 584
567 585 def parent_issue_id=(arg)
568 586 parent_issue_id = arg.blank? ? nil : arg.to_i
569 587 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
570 588 @parent_issue.id
571 589 else
572 590 @parent_issue = nil
573 591 nil
574 592 end
575 593 end
576 594
577 595 def parent_issue_id
578 596 if instance_variable_defined? :@parent_issue
579 597 @parent_issue.nil? ? nil : @parent_issue.id
580 598 else
581 599 parent_id
582 600 end
583 601 end
584 602
585 603 # Extracted from the ReportsController.
586 604 def self.by_tracker(project)
587 605 count_and_group_by(:project => project,
588 606 :field => 'tracker_id',
589 607 :joins => Tracker.table_name)
590 608 end
591 609
592 610 def self.by_version(project)
593 611 count_and_group_by(:project => project,
594 612 :field => 'fixed_version_id',
595 613 :joins => Version.table_name)
596 614 end
597 615
598 616 def self.by_priority(project)
599 617 count_and_group_by(:project => project,
600 618 :field => 'priority_id',
601 619 :joins => IssuePriority.table_name)
602 620 end
603 621
604 622 def self.by_category(project)
605 623 count_and_group_by(:project => project,
606 624 :field => 'category_id',
607 625 :joins => IssueCategory.table_name)
608 626 end
609 627
610 628 def self.by_assigned_to(project)
611 629 count_and_group_by(:project => project,
612 630 :field => 'assigned_to_id',
613 631 :joins => User.table_name)
614 632 end
615 633
616 634 def self.by_author(project)
617 635 count_and_group_by(:project => project,
618 636 :field => 'author_id',
619 637 :joins => User.table_name)
620 638 end
621 639
622 640 def self.by_subproject(project)
623 641 ActiveRecord::Base.connection.select_all("select s.id as status_id,
624 642 s.is_closed as closed,
625 643 i.project_id as project_id,
626 644 count(i.id) as total
627 645 from
628 646 #{Issue.table_name} i, #{IssueStatus.table_name} s
629 647 where
630 648 i.status_id=s.id
631 649 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
632 650 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
633 651 end
634 652 # End ReportsController extraction
635 653
636 654 # Returns an array of projects that current user can move issues to
637 655 def self.allowed_target_projects_on_move
638 656 projects = []
639 657 if User.current.admin?
640 658 # admin is allowed to move issues to any active (visible) project
641 659 projects = Project.visible.all
642 660 elsif User.current.logged?
643 661 if Role.non_member.allowed_to?(:move_issues)
644 662 projects = Project.visible.all
645 663 else
646 664 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
647 665 end
648 666 end
649 667 projects
650 668 end
651 669
652 670 private
653 671
654 672 def update_nested_set_attributes
655 673 if root_id.nil?
656 674 # issue was just created
657 675 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
658 676 set_default_left_and_right
659 677 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
660 678 if @parent_issue
661 679 move_to_child_of(@parent_issue)
662 680 end
663 681 reload
664 682 elsif parent_issue_id != parent_id
665 683 former_parent_id = parent_id
666 684 # moving an existing issue
667 685 if @parent_issue && @parent_issue.root_id == root_id
668 686 # inside the same tree
669 687 move_to_child_of(@parent_issue)
670 688 else
671 689 # to another tree
672 690 unless root?
673 691 move_to_right_of(root)
674 692 reload
675 693 end
676 694 old_root_id = root_id
677 695 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
678 696 target_maxright = nested_set_scope.maximum(right_column_name) || 0
679 697 offset = target_maxright + 1 - lft
680 698 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
681 699 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
682 700 self[left_column_name] = lft + offset
683 701 self[right_column_name] = rgt + offset
684 702 if @parent_issue
685 703 move_to_child_of(@parent_issue)
686 704 end
687 705 end
688 706 reload
689 707 # delete invalid relations of all descendants
690 708 self_and_descendants.each do |issue|
691 709 issue.relations.each do |relation|
692 710 relation.destroy unless relation.valid?
693 711 end
694 712 end
695 713 # update former parent
696 714 recalculate_attributes_for(former_parent_id) if former_parent_id
697 715 end
698 716 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
699 717 end
700 718
701 719 def update_parent_attributes
702 720 recalculate_attributes_for(parent_id) if parent_id
703 721 end
704 722
705 723 def recalculate_attributes_for(issue_id)
706 724 if issue_id && p = Issue.find_by_id(issue_id)
707 725 # priority = highest priority of children
708 726 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
709 727 p.priority = IssuePriority.find_by_position(priority_position)
710 728 end
711 729
712 730 # start/due dates = lowest/highest dates of children
713 731 p.start_date = p.children.minimum(:start_date)
714 732 p.due_date = p.children.maximum(:due_date)
715 733 if p.start_date && p.due_date && p.due_date < p.start_date
716 734 p.start_date, p.due_date = p.due_date, p.start_date
717 735 end
718 736
719 737 # done ratio = weighted average ratio of leaves
720 738 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
721 739 leaves_count = p.leaves.count
722 740 if leaves_count > 0
723 741 average = p.leaves.average(:estimated_hours).to_f
724 742 if average == 0
725 743 average = 1
726 744 end
727 745 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
728 746 progress = done / (average * leaves_count)
729 747 p.done_ratio = progress.round
730 748 end
731 749 end
732 750
733 751 # estimate = sum of leaves estimates
734 752 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
735 753 p.estimated_hours = nil if p.estimated_hours == 0.0
736 754
737 755 # ancestors will be recursively updated
738 756 p.save(false)
739 757 end
740 758 end
741 759
742 760 def destroy_children
743 761 unless leaf?
744 762 children.each do |child|
745 763 child.destroy
746 764 end
747 765 end
748 766 end
749 767
750 768 # Update issues so their versions are not pointing to a
751 769 # fixed_version that is not shared with the issue's project
752 770 def self.update_versions(conditions=nil)
753 771 # Only need to update issues with a fixed_version from
754 772 # a different project and that is not systemwide shared
755 773 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
756 774 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
757 775 " AND #{Version.table_name}.sharing <> 'system'",
758 776 conditions),
759 777 :include => [:project, :fixed_version]
760 778 ).each do |issue|
761 779 next if issue.project.nil? || issue.fixed_version.nil?
762 780 unless issue.project.shared_versions.include?(issue.fixed_version)
763 781 issue.init_journal(User.current)
764 782 issue.fixed_version = nil
765 783 issue.save
766 784 end
767 785 end
768 786 end
769 787
770 788 # Callback on attachment deletion
771 789 def attachment_removed(obj)
772 790 journal = init_journal(User.current)
773 791 journal.details << JournalDetail.new(:property => 'attachment',
774 792 :prop_key => obj.id,
775 793 :old_value => obj.filename)
776 794 journal.save
777 795 end
778 796
779 797 # Default assignment based on category
780 798 def default_assign
781 799 if assigned_to.nil? && category && category.assigned_to
782 800 self.assigned_to = category.assigned_to
783 801 end
784 802 end
785 803
786 804 # Updates start/due dates of following issues
787 805 def reschedule_following_issues
788 806 if start_date_changed? || due_date_changed?
789 807 relations_from.each do |relation|
790 808 relation.set_issue_to_dates
791 809 end
792 810 end
793 811 end
794 812
795 813 # Closes duplicates if the issue is being closed
796 814 def close_duplicates
797 815 if closing?
798 816 duplicates.each do |duplicate|
799 817 # Reload is need in case the duplicate was updated by a previous duplicate
800 818 duplicate.reload
801 819 # Don't re-close it if it's already closed
802 820 next if duplicate.closed?
803 821 # Same user and notes
804 822 if @current_journal
805 823 duplicate.init_journal(@current_journal.user, @current_journal.notes)
806 824 end
807 825 duplicate.update_attribute :status, self.status
808 826 end
809 827 end
810 828 end
811 829
812 830 # Saves the changes in a Journal
813 831 # Called after_save
814 832 def create_journal
815 833 if @current_journal
816 834 # attributes changes
817 835 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
818 836 @current_journal.details << JournalDetail.new(:property => 'attr',
819 837 :prop_key => c,
820 838 :old_value => @issue_before_change.send(c),
821 839 :value => send(c)) unless send(c)==@issue_before_change.send(c)
822 840 }
823 841 # custom fields changes
824 842 custom_values.each {|c|
825 843 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
826 844 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
827 845 @current_journal.details << JournalDetail.new(:property => 'cf',
828 846 :prop_key => c.custom_field_id,
829 847 :old_value => @custom_values_before_change[c.custom_field_id],
830 848 :value => c.value)
831 849 }
832 850 @current_journal.save
833 851 # reset current journal
834 852 init_journal @current_journal.user, @current_journal.notes
835 853 end
836 854 end
837 855
838 856 # Query generator for selecting groups of issue counts for a project
839 857 # based on specific criteria
840 858 #
841 859 # Options
842 860 # * project - Project to search in.
843 861 # * field - String. Issue field to key off of in the grouping.
844 862 # * joins - String. The table name to join against.
845 863 def self.count_and_group_by(options)
846 864 project = options.delete(:project)
847 865 select_field = options.delete(:field)
848 866 joins = options.delete(:joins)
849 867
850 868 where = "i.#{select_field}=j.id"
851 869
852 870 ActiveRecord::Base.connection.select_all("select s.id as status_id,
853 871 s.is_closed as closed,
854 872 j.id as #{select_field},
855 873 count(i.id) as total
856 874 from
857 875 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
858 876 where
859 877 i.status_id=s.id
860 878 and #{where}
861 879 and i.project_id=#{project.id}
862 880 group by s.id, s.is_closed, j.id")
863 881 end
864 882
865 883
866 884 end
@@ -1,1172 +1,1271
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 < ActionController::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :member_roles,
30 30 :issues,
31 31 :issue_statuses,
32 32 :versions,
33 33 :trackers,
34 34 :projects_trackers,
35 35 :issue_categories,
36 36 :enabled_modules,
37 37 :enumerations,
38 38 :attachments,
39 39 :workflows,
40 40 :custom_fields,
41 41 :custom_values,
42 42 :custom_fields_projects,
43 43 :custom_fields_trackers,
44 44 :time_entries,
45 45 :journals,
46 46 :journal_details,
47 47 :queries
48 48
49 49 def setup
50 50 @controller = IssuesController.new
51 51 @request = ActionController::TestRequest.new
52 52 @response = ActionController::TestResponse.new
53 53 User.current = nil
54 54 end
55 55
56 56 def test_index
57 57 Setting.default_language = 'en'
58 58
59 59 get :index
60 60 assert_response :success
61 61 assert_template 'index.rhtml'
62 62 assert_not_nil assigns(:issues)
63 63 assert_nil assigns(:project)
64 64 assert_tag :tag => 'a', :content => /Can't print recipes/
65 65 assert_tag :tag => 'a', :content => /Subproject issue/
66 66 # private projects hidden
67 67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
68 68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
69 69 # project column
70 70 assert_tag :tag => 'th', :content => /Project/
71 71 end
72 72
73 73 def test_index_should_not_list_issues_when_module_disabled
74 74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 75 get :index
76 76 assert_response :success
77 77 assert_template 'index.rhtml'
78 78 assert_not_nil assigns(:issues)
79 79 assert_nil assigns(:project)
80 80 assert_no_tag :tag => 'a', :content => /Can't print recipes/
81 81 assert_tag :tag => 'a', :content => /Subproject issue/
82 82 end
83 83
84 84 def test_index_should_not_list_issues_when_module_disabled
85 85 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
86 86 get :index
87 87 assert_response :success
88 88 assert_template 'index.rhtml'
89 89 assert_not_nil assigns(:issues)
90 90 assert_nil assigns(:project)
91 91 assert_no_tag :tag => 'a', :content => /Can't print recipes/
92 92 assert_tag :tag => 'a', :content => /Subproject issue/
93 93 end
94 94
95 95 def test_index_with_project
96 96 Setting.display_subprojects_issues = 0
97 97 get :index, :project_id => 1
98 98 assert_response :success
99 99 assert_template 'index.rhtml'
100 100 assert_not_nil assigns(:issues)
101 101 assert_tag :tag => 'a', :content => /Can't print recipes/
102 102 assert_no_tag :tag => 'a', :content => /Subproject issue/
103 103 end
104 104
105 105 def test_index_with_project_and_subprojects
106 106 Setting.display_subprojects_issues = 1
107 107 get :index, :project_id => 1
108 108 assert_response :success
109 109 assert_template 'index.rhtml'
110 110 assert_not_nil assigns(:issues)
111 111 assert_tag :tag => 'a', :content => /Can't print recipes/
112 112 assert_tag :tag => 'a', :content => /Subproject issue/
113 113 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
114 114 end
115 115
116 116 def test_index_with_project_and_subprojects_should_show_private_subprojects
117 117 @request.session[:user_id] = 2
118 118 Setting.display_subprojects_issues = 1
119 119 get :index, :project_id => 1
120 120 assert_response :success
121 121 assert_template 'index.rhtml'
122 122 assert_not_nil assigns(:issues)
123 123 assert_tag :tag => 'a', :content => /Can't print recipes/
124 124 assert_tag :tag => 'a', :content => /Subproject issue/
125 125 assert_tag :tag => 'a', :content => /Issue of a private subproject/
126 126 end
127 127
128 128 def test_index_with_project_and_default_filter
129 129 get :index, :project_id => 1, :set_filter => 1
130 130 assert_response :success
131 131 assert_template 'index.rhtml'
132 132 assert_not_nil assigns(:issues)
133 133
134 134 query = assigns(:query)
135 135 assert_not_nil query
136 136 # default filter
137 137 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
138 138 end
139 139
140 140 def test_index_with_project_and_filter
141 141 get :index, :project_id => 1, :set_filter => 1,
142 142 :fields => ['tracker_id'],
143 143 :operators => {'tracker_id' => '='},
144 144 :values => {'tracker_id' => ['1']}
145 145 assert_response :success
146 146 assert_template 'index.rhtml'
147 147 assert_not_nil assigns(:issues)
148 148
149 149 query = assigns(:query)
150 150 assert_not_nil query
151 151 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
152 152 end
153 153
154 154 def test_index_with_project_and_empty_filters
155 155 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
156 156 assert_response :success
157 157 assert_template 'index.rhtml'
158 158 assert_not_nil assigns(:issues)
159 159
160 160 query = assigns(:query)
161 161 assert_not_nil query
162 162 # no filter
163 163 assert_equal({}, query.filters)
164 164 end
165 165
166 166 def test_index_with_query
167 167 get :index, :project_id => 1, :query_id => 5
168 168 assert_response :success
169 169 assert_template 'index.rhtml'
170 170 assert_not_nil assigns(:issues)
171 171 assert_nil assigns(:issue_count_by_group)
172 172 end
173 173
174 174 def test_index_with_query_grouped_by_tracker
175 175 get :index, :project_id => 1, :query_id => 6
176 176 assert_response :success
177 177 assert_template 'index.rhtml'
178 178 assert_not_nil assigns(:issues)
179 179 assert_not_nil assigns(:issue_count_by_group)
180 180 end
181 181
182 182 def test_index_with_query_grouped_by_list_custom_field
183 183 get :index, :project_id => 1, :query_id => 9
184 184 assert_response :success
185 185 assert_template 'index.rhtml'
186 186 assert_not_nil assigns(:issues)
187 187 assert_not_nil assigns(:issue_count_by_group)
188 188 end
189 189
190 190 def test_index_sort_by_field_not_included_in_columns
191 191 Setting.issue_list_default_columns = %w(subject author)
192 192 get :index, :sort => 'tracker'
193 193 end
194 194
195 195 def test_index_csv_with_project
196 196 Setting.default_language = 'en'
197 197
198 198 get :index, :format => 'csv'
199 199 assert_response :success
200 200 assert_not_nil assigns(:issues)
201 201 assert_equal 'text/csv', @response.content_type
202 202 assert @response.body.starts_with?("#,")
203 203
204 204 get :index, :project_id => 1, :format => 'csv'
205 205 assert_response :success
206 206 assert_not_nil assigns(:issues)
207 207 assert_equal 'text/csv', @response.content_type
208 208 end
209 209
210 210 def test_index_pdf
211 211 get :index, :format => 'pdf'
212 212 assert_response :success
213 213 assert_not_nil assigns(:issues)
214 214 assert_equal 'application/pdf', @response.content_type
215 215
216 216 get :index, :project_id => 1, :format => 'pdf'
217 217 assert_response :success
218 218 assert_not_nil assigns(:issues)
219 219 assert_equal 'application/pdf', @response.content_type
220 220
221 221 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
222 222 assert_response :success
223 223 assert_not_nil assigns(:issues)
224 224 assert_equal 'application/pdf', @response.content_type
225 225 end
226 226
227 227 def test_index_pdf_with_query_grouped_by_list_custom_field
228 228 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
229 229 assert_response :success
230 230 assert_not_nil assigns(:issues)
231 231 assert_not_nil assigns(:issue_count_by_group)
232 232 assert_equal 'application/pdf', @response.content_type
233 233 end
234 234
235 235 def test_index_sort
236 236 get :index, :sort => 'tracker,id:desc'
237 237 assert_response :success
238 238
239 239 sort_params = @request.session['issues_index_sort']
240 240 assert sort_params.is_a?(String)
241 241 assert_equal 'tracker,id:desc', sort_params
242 242
243 243 issues = assigns(:issues)
244 244 assert_not_nil issues
245 245 assert !issues.empty?
246 246 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
247 247 end
248 248
249 249 def test_index_with_columns
250 250 columns = ['tracker', 'subject', 'assigned_to']
251 251 get :index, :set_filter => 1, :query => { 'column_names' => columns}
252 252 assert_response :success
253 253
254 254 # query should use specified columns
255 255 query = assigns(:query)
256 256 assert_kind_of Query, query
257 257 assert_equal columns, query.column_names.map(&:to_s)
258 258
259 259 # columns should be stored in session
260 260 assert_kind_of Hash, session[:query]
261 261 assert_kind_of Array, session[:query][:column_names]
262 262 assert_equal columns, session[:query][:column_names].map(&:to_s)
263 263 end
264 264
265 265 def test_show_by_anonymous
266 266 get :show, :id => 1
267 267 assert_response :success
268 268 assert_template 'show.rhtml'
269 269 assert_not_nil assigns(:issue)
270 270 assert_equal Issue.find(1), assigns(:issue)
271 271
272 272 # anonymous role is allowed to add a note
273 273 assert_tag :tag => 'form',
274 274 :descendant => { :tag => 'fieldset',
275 275 :child => { :tag => 'legend',
276 276 :content => /Notes/ } }
277 277 end
278 278
279 279 def test_show_by_manager
280 280 @request.session[:user_id] = 2
281 281 get :show, :id => 1
282 282 assert_response :success
283 283
284 284 assert_tag :tag => 'form',
285 285 :descendant => { :tag => 'fieldset',
286 286 :child => { :tag => 'legend',
287 287 :content => /Change properties/ } },
288 288 :descendant => { :tag => 'fieldset',
289 289 :child => { :tag => 'legend',
290 290 :content => /Log time/ } },
291 291 :descendant => { :tag => 'fieldset',
292 292 :child => { :tag => 'legend',
293 293 :content => /Notes/ } }
294 294 end
295 295
296 296 def test_show_should_deny_anonymous_access_without_permission
297 297 Role.anonymous.remove_permission!(:view_issues)
298 298 get :show, :id => 1
299 299 assert_response :redirect
300 300 end
301 301
302 302 def test_show_should_deny_non_member_access_without_permission
303 303 Role.non_member.remove_permission!(:view_issues)
304 304 @request.session[:user_id] = 9
305 305 get :show, :id => 1
306 306 assert_response 403
307 307 end
308 308
309 309 def test_show_should_deny_member_access_without_permission
310 310 Role.find(1).remove_permission!(:view_issues)
311 311 @request.session[:user_id] = 2
312 312 get :show, :id => 1
313 313 assert_response 403
314 314 end
315 315
316 316 def test_show_should_not_disclose_relations_to_invisible_issues
317 317 Setting.cross_project_issue_relations = '1'
318 318 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
319 319 # Relation to a private project issue
320 320 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
321 321
322 322 get :show, :id => 1
323 323 assert_response :success
324 324
325 325 assert_tag :div, :attributes => { :id => 'relations' },
326 326 :descendant => { :tag => 'a', :content => /#2$/ }
327 327 assert_no_tag :div, :attributes => { :id => 'relations' },
328 328 :descendant => { :tag => 'a', :content => /#4$/ }
329 329 end
330 330
331 331 def test_show_atom
332 332 get :show, :id => 2, :format => 'atom'
333 333 assert_response :success
334 334 assert_template 'journals/index.rxml'
335 335 # Inline image
336 336 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
337 337 end
338 338
339 339 def test_show_export_to_pdf
340 340 get :show, :id => 3, :format => 'pdf'
341 341 assert_response :success
342 342 assert_equal 'application/pdf', @response.content_type
343 343 assert @response.body.starts_with?('%PDF')
344 344 assert_not_nil assigns(:issue)
345 345 end
346 346
347 347 def test_get_new
348 348 @request.session[:user_id] = 2
349 349 get :new, :project_id => 1, :tracker_id => 1
350 350 assert_response :success
351 351 assert_template 'new'
352 352
353 353 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
354 354 :value => 'Default string' }
355 355 end
356 356
357 357 def test_get_new_without_tracker_id
358 358 @request.session[:user_id] = 2
359 359 get :new, :project_id => 1
360 360 assert_response :success
361 361 assert_template 'new'
362 362
363 363 issue = assigns(:issue)
364 364 assert_not_nil issue
365 365 assert_equal Project.find(1).trackers.first, issue.tracker
366 366 end
367 367
368 368 def test_get_new_with_no_default_status_should_display_an_error
369 369 @request.session[:user_id] = 2
370 370 IssueStatus.delete_all
371 371
372 372 get :new, :project_id => 1
373 373 assert_response 500
374 374 assert_error_tag :content => /No default issue/
375 375 end
376 376
377 377 def test_get_new_with_no_tracker_should_display_an_error
378 378 @request.session[:user_id] = 2
379 379 Tracker.delete_all
380 380
381 381 get :new, :project_id => 1
382 382 assert_response 500
383 383 assert_error_tag :content => /No tracker/
384 384 end
385 385
386 386 def test_update_new_form
387 387 @request.session[:user_id] = 2
388 388 xhr :post, :new, :project_id => 1,
389 389 :issue => {:tracker_id => 2,
390 390 :subject => 'This is the test_new issue',
391 391 :description => 'This is the description',
392 392 :priority_id => 5}
393 393 assert_response :success
394 394 assert_template 'attributes'
395 395
396 396 issue = assigns(:issue)
397 397 assert_kind_of Issue, issue
398 398 assert_equal 1, issue.project_id
399 399 assert_equal 2, issue.tracker_id
400 400 assert_equal 'This is the test_new issue', issue.subject
401 401 end
402 402
403 403 def test_post_create
404 404 @request.session[:user_id] = 2
405 405 assert_difference 'Issue.count' do
406 406 post :create, :project_id => 1,
407 407 :issue => {:tracker_id => 3,
408 408 :status_id => 2,
409 409 :subject => 'This is the test_new issue',
410 410 :description => 'This is the description',
411 411 :priority_id => 5,
412 412 :start_date => '2010-11-07',
413 413 :estimated_hours => '',
414 414 :custom_field_values => {'2' => 'Value for field 2'}}
415 415 end
416 416 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
417 417
418 418 issue = Issue.find_by_subject('This is the test_new issue')
419 419 assert_not_nil issue
420 420 assert_equal 2, issue.author_id
421 421 assert_equal 3, issue.tracker_id
422 422 assert_equal 2, issue.status_id
423 423 assert_equal Date.parse('2010-11-07'), issue.start_date
424 424 assert_nil issue.estimated_hours
425 425 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
426 426 assert_not_nil v
427 427 assert_equal 'Value for field 2', v.value
428 428 end
429 429
430 430 def test_post_create_without_start_date
431 431 @request.session[:user_id] = 2
432 432 assert_difference 'Issue.count' do
433 433 post :create, :project_id => 1,
434 434 :issue => {:tracker_id => 3,
435 435 :status_id => 2,
436 436 :subject => 'This is the test_new issue',
437 437 :description => 'This is the description',
438 438 :priority_id => 5,
439 439 :start_date => '',
440 440 :estimated_hours => '',
441 441 :custom_field_values => {'2' => 'Value for field 2'}}
442 442 end
443 443 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
444 444
445 445 issue = Issue.find_by_subject('This is the test_new issue')
446 446 assert_not_nil issue
447 447 assert_nil issue.start_date
448 448 end
449 449
450 450 def test_post_create_and_continue
451 451 @request.session[:user_id] = 2
452 452 post :create, :project_id => 1,
453 453 :issue => {:tracker_id => 3,
454 454 :subject => 'This is first issue',
455 455 :priority_id => 5},
456 456 :continue => ''
457 457 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook',
458 458 :issue => {:tracker_id => 3}
459 459 end
460 460
461 461 def test_post_create_without_custom_fields_param
462 462 @request.session[:user_id] = 2
463 463 assert_difference 'Issue.count' do
464 464 post :create, :project_id => 1,
465 465 :issue => {:tracker_id => 1,
466 466 :subject => 'This is the test_new issue',
467 467 :description => 'This is the description',
468 468 :priority_id => 5}
469 469 end
470 470 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
471 471 end
472 472
473 473 def test_post_create_with_required_custom_field_and_without_custom_fields_param
474 474 field = IssueCustomField.find_by_name('Database')
475 475 field.update_attribute(:is_required, true)
476 476
477 477 @request.session[:user_id] = 2
478 478 post :create, :project_id => 1,
479 479 :issue => {:tracker_id => 1,
480 480 :subject => 'This is the test_new issue',
481 481 :description => 'This is the description',
482 482 :priority_id => 5}
483 483 assert_response :success
484 484 assert_template 'new'
485 485 issue = assigns(:issue)
486 486 assert_not_nil issue
487 487 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
488 488 end
489 489
490 490 def test_post_create_with_watchers
491 491 @request.session[:user_id] = 2
492 492 ActionMailer::Base.deliveries.clear
493 493
494 494 assert_difference 'Watcher.count', 2 do
495 495 post :create, :project_id => 1,
496 496 :issue => {:tracker_id => 1,
497 497 :subject => 'This is a new issue with watchers',
498 498 :description => 'This is the description',
499 499 :priority_id => 5,
500 500 :watcher_user_ids => ['2', '3']}
501 501 end
502 502 issue = Issue.find_by_subject('This is a new issue with watchers')
503 503 assert_not_nil issue
504 504 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
505 505
506 506 # Watchers added
507 507 assert_equal [2, 3], issue.watcher_user_ids.sort
508 508 assert issue.watched_by?(User.find(3))
509 509 # Watchers notified
510 510 mail = ActionMailer::Base.deliveries.last
511 511 assert_kind_of TMail::Mail, mail
512 512 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
513 513 end
514 514
515 515 def test_post_create_subissue
516 516 @request.session[:user_id] = 2
517 517
518 518 assert_difference 'Issue.count' do
519 519 post :create, :project_id => 1,
520 520 :issue => {:tracker_id => 1,
521 521 :subject => 'This is a child issue',
522 522 :parent_issue_id => 2}
523 523 end
524 524 issue = Issue.find_by_subject('This is a child issue')
525 525 assert_not_nil issue
526 526 assert_equal Issue.find(2), issue.parent
527 527 end
528 528
529 529 def test_post_create_should_send_a_notification
530 530 ActionMailer::Base.deliveries.clear
531 531 @request.session[:user_id] = 2
532 532 assert_difference 'Issue.count' do
533 533 post :create, :project_id => 1,
534 534 :issue => {:tracker_id => 3,
535 535 :subject => 'This is the test_new issue',
536 536 :description => 'This is the description',
537 537 :priority_id => 5,
538 538 :estimated_hours => '',
539 539 :custom_field_values => {'2' => 'Value for field 2'}}
540 540 end
541 541 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
542 542
543 543 assert_equal 1, ActionMailer::Base.deliveries.size
544 544 end
545 545
546 546 def test_post_create_should_preserve_fields_values_on_validation_failure
547 547 @request.session[:user_id] = 2
548 548 post :create, :project_id => 1,
549 549 :issue => {:tracker_id => 1,
550 550 # empty subject
551 551 :subject => '',
552 552 :description => 'This is a description',
553 553 :priority_id => 6,
554 554 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
555 555 assert_response :success
556 556 assert_template 'new'
557 557
558 558 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
559 559 :content => 'This is a description'
560 560 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
561 561 :child => { :tag => 'option', :attributes => { :selected => 'selected',
562 562 :value => '6' },
563 563 :content => 'High' }
564 564 # Custom fields
565 565 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
566 566 :child => { :tag => 'option', :attributes => { :selected => 'selected',
567 567 :value => 'Oracle' },
568 568 :content => 'Oracle' }
569 569 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
570 570 :value => 'Value for field 2'}
571 571 end
572 572
573 573 def test_post_create_should_ignore_non_safe_attributes
574 574 @request.session[:user_id] = 2
575 575 assert_nothing_raised do
576 576 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
577 577 end
578 578 end
579 579
580 580 context "without workflow privilege" do
581 581 setup do
582 582 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
583 Role.anonymous.add_permission! :add_issues
583 Role.anonymous.add_permission! :add_issues, :add_issue_notes
584 584 end
585 585
586 586 context "#new" do
587 587 should "propose default status only" do
588 588 get :new, :project_id => 1
589 589 assert_response :success
590 590 assert_template 'new'
591 591 assert_tag :tag => 'select',
592 592 :attributes => {:name => 'issue[status_id]'},
593 593 :children => {:count => 1},
594 594 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
595 595 end
596 596
597 597 should "accept default status" do
598 598 assert_difference 'Issue.count' do
599 599 post :create, :project_id => 1,
600 600 :issue => {:tracker_id => 1,
601 601 :subject => 'This is an issue',
602 602 :status_id => 1}
603 603 end
604 604 issue = Issue.last(:order => 'id')
605 605 assert_equal IssueStatus.default, issue.status
606 606 end
607 607
608 should "accept default status" do
609 assert_difference 'Issue.count' do
610 post :create, :project_id => 1,
611 :issue => {:tracker_id => 1,
612 :subject => 'This is an issue',
613 :status_id => 1}
614 end
615 issue = Issue.last(:order => 'id')
616 assert_equal IssueStatus.default, issue.status
617 end
618
608 619 should "ignore unauthorized status" do
609 620 assert_difference 'Issue.count' do
610 621 post :create, :project_id => 1,
611 622 :issue => {:tracker_id => 1,
612 623 :subject => 'This is an issue',
613 624 :status_id => 3}
614 625 end
615 626 issue = Issue.last(:order => 'id')
616 627 assert_equal IssueStatus.default, issue.status
617 628 end
618 629 end
630
631 context "#update" do
632 should "ignore status change" do
633 assert_difference 'Journal.count' do
634 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
635 end
636 assert_equal 1, Issue.find(1).status_id
637 end
638
639 should "ignore attributes changes" do
640 assert_difference 'Journal.count' do
641 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
642 end
643 issue = Issue.find(1)
644 assert_equal "Can't print recipes", issue.subject
645 assert_nil issue.assigned_to
646 end
647 end
648 end
649
650 context "with workflow privilege" do
651 setup do
652 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
653 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
654 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
655 Role.anonymous.add_permission! :add_issues, :add_issue_notes
656 end
657
658 context "#update" do
659 should "accept authorized status" do
660 assert_difference 'Journal.count' do
661 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
662 end
663 assert_equal 3, Issue.find(1).status_id
664 end
665
666 should "ignore unauthorized status" do
667 assert_difference 'Journal.count' do
668 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
669 end
670 assert_equal 1, Issue.find(1).status_id
671 end
672
673 should "accept authorized attributes changes" do
674 assert_difference 'Journal.count' do
675 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
676 end
677 issue = Issue.find(1)
678 assert_equal 2, issue.assigned_to_id
679 end
680
681 should "ignore unauthorized attributes changes" do
682 assert_difference 'Journal.count' do
683 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
684 end
685 issue = Issue.find(1)
686 assert_equal "Can't print recipes", issue.subject
687 end
688 end
689
690 context "and :edit_issues permission" do
691 setup do
692 Role.anonymous.add_permission! :add_issues, :edit_issues
693 end
694
695 should "accept authorized status" do
696 assert_difference 'Journal.count' do
697 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
698 end
699 assert_equal 3, Issue.find(1).status_id
700 end
701
702 should "ignore unauthorized status" do
703 assert_difference 'Journal.count' do
704 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
705 end
706 assert_equal 1, Issue.find(1).status_id
707 end
708
709 should "accept authorized attributes changes" do
710 assert_difference 'Journal.count' do
711 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
712 end
713 issue = Issue.find(1)
714 assert_equal "changed", issue.subject
715 assert_equal 2, issue.assigned_to_id
716 end
717 end
619 718 end
620 719
621 720 def test_copy_issue
622 721 @request.session[:user_id] = 2
623 722 get :new, :project_id => 1, :copy_from => 1
624 723 assert_template 'new'
625 724 assert_not_nil assigns(:issue)
626 725 orig = Issue.find(1)
627 726 assert_equal orig.subject, assigns(:issue).subject
628 727 end
629 728
630 729 def test_get_edit
631 730 @request.session[:user_id] = 2
632 731 get :edit, :id => 1
633 732 assert_response :success
634 733 assert_template 'edit'
635 734 assert_not_nil assigns(:issue)
636 735 assert_equal Issue.find(1), assigns(:issue)
637 736 end
638 737
639 738 def test_get_edit_with_params
640 739 @request.session[:user_id] = 2
641 740 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
642 741 assert_response :success
643 742 assert_template 'edit'
644 743
645 744 issue = assigns(:issue)
646 745 assert_not_nil issue
647 746
648 747 assert_equal 5, issue.status_id
649 748 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
650 749 :child => { :tag => 'option',
651 750 :content => 'Closed',
652 751 :attributes => { :selected => 'selected' } }
653 752
654 753 assert_equal 7, issue.priority_id
655 754 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
656 755 :child => { :tag => 'option',
657 756 :content => 'Urgent',
658 757 :attributes => { :selected => 'selected' } }
659 758 end
660 759
661 760 def test_update_edit_form
662 761 @request.session[:user_id] = 2
663 762 xhr :post, :new, :project_id => 1,
664 763 :id => 1,
665 764 :issue => {:tracker_id => 2,
666 765 :subject => 'This is the test_new issue',
667 766 :description => 'This is the description',
668 767 :priority_id => 5}
669 768 assert_response :success
670 769 assert_template 'attributes'
671 770
672 771 issue = assigns(:issue)
673 772 assert_kind_of Issue, issue
674 773 assert_equal 1, issue.id
675 774 assert_equal 1, issue.project_id
676 775 assert_equal 2, issue.tracker_id
677 776 assert_equal 'This is the test_new issue', issue.subject
678 777 end
679 778
680 779 def test_update_using_invalid_http_verbs
681 780 @request.session[:user_id] = 2
682 781 subject = 'Updated by an invalid http verb'
683 782
684 783 get :update, :id => 1, :issue => {:subject => subject}
685 784 assert_not_equal subject, Issue.find(1).subject
686 785
687 786 post :update, :id => 1, :issue => {:subject => subject}
688 787 assert_not_equal subject, Issue.find(1).subject
689 788
690 789 delete :update, :id => 1, :issue => {:subject => subject}
691 790 assert_not_equal subject, Issue.find(1).subject
692 791 end
693 792
694 793 def test_put_update_without_custom_fields_param
695 794 @request.session[:user_id] = 2
696 795 ActionMailer::Base.deliveries.clear
697 796
698 797 issue = Issue.find(1)
699 798 assert_equal '125', issue.custom_value_for(2).value
700 799 old_subject = issue.subject
701 800 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
702 801
703 802 assert_difference('Journal.count') do
704 803 assert_difference('JournalDetail.count', 2) do
705 804 put :update, :id => 1, :issue => {:subject => new_subject,
706 805 :priority_id => '6',
707 806 :category_id => '1' # no change
708 807 }
709 808 end
710 809 end
711 810 assert_redirected_to :action => 'show', :id => '1'
712 811 issue.reload
713 812 assert_equal new_subject, issue.subject
714 813 # Make sure custom fields were not cleared
715 814 assert_equal '125', issue.custom_value_for(2).value
716 815
717 816 mail = ActionMailer::Base.deliveries.last
718 817 assert_kind_of TMail::Mail, mail
719 818 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
720 819 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
721 820 end
722 821
723 822 def test_put_update_with_custom_field_change
724 823 @request.session[:user_id] = 2
725 824 issue = Issue.find(1)
726 825 assert_equal '125', issue.custom_value_for(2).value
727 826
728 827 assert_difference('Journal.count') do
729 828 assert_difference('JournalDetail.count', 3) do
730 829 put :update, :id => 1, :issue => {:subject => 'Custom field change',
731 830 :priority_id => '6',
732 831 :category_id => '1', # no change
733 832 :custom_field_values => { '2' => 'New custom value' }
734 833 }
735 834 end
736 835 end
737 836 assert_redirected_to :action => 'show', :id => '1'
738 837 issue.reload
739 838 assert_equal 'New custom value', issue.custom_value_for(2).value
740 839
741 840 mail = ActionMailer::Base.deliveries.last
742 841 assert_kind_of TMail::Mail, mail
743 842 assert mail.body.include?("Searchable field changed from 125 to New custom value")
744 843 end
745 844
746 845 def test_put_update_with_status_and_assignee_change
747 846 issue = Issue.find(1)
748 847 assert_equal 1, issue.status_id
749 848 @request.session[:user_id] = 2
750 849 assert_difference('TimeEntry.count', 0) do
751 850 put :update,
752 851 :id => 1,
753 852 :issue => { :status_id => 2, :assigned_to_id => 3 },
754 853 :notes => 'Assigned to dlopper',
755 854 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
756 855 end
757 856 assert_redirected_to :action => 'show', :id => '1'
758 857 issue.reload
759 858 assert_equal 2, issue.status_id
760 859 j = Journal.find(:first, :order => 'id DESC')
761 860 assert_equal 'Assigned to dlopper', j.notes
762 861 assert_equal 2, j.details.size
763 862
764 863 mail = ActionMailer::Base.deliveries.last
765 864 assert mail.body.include?("Status changed from New to Assigned")
766 865 # subject should contain the new status
767 866 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
768 867 end
769 868
770 869 def test_put_update_with_note_only
771 870 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
772 871 # anonymous user
773 872 put :update,
774 873 :id => 1,
775 874 :notes => notes
776 875 assert_redirected_to :action => 'show', :id => '1'
777 876 j = Journal.find(:first, :order => 'id DESC')
778 877 assert_equal notes, j.notes
779 878 assert_equal 0, j.details.size
780 879 assert_equal User.anonymous, j.user
781 880
782 881 mail = ActionMailer::Base.deliveries.last
783 882 assert mail.body.include?(notes)
784 883 end
785 884
786 885 def test_put_update_with_note_and_spent_time
787 886 @request.session[:user_id] = 2
788 887 spent_hours_before = Issue.find(1).spent_hours
789 888 assert_difference('TimeEntry.count') do
790 889 put :update,
791 890 :id => 1,
792 891 :notes => '2.5 hours added',
793 892 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
794 893 end
795 894 assert_redirected_to :action => 'show', :id => '1'
796 895
797 896 issue = Issue.find(1)
798 897
799 898 j = Journal.find(:first, :order => 'id DESC')
800 899 assert_equal '2.5 hours added', j.notes
801 900 assert_equal 0, j.details.size
802 901
803 902 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
804 903 assert_not_nil t
805 904 assert_equal 2.5, t.hours
806 905 assert_equal spent_hours_before + 2.5, issue.spent_hours
807 906 end
808 907
809 908 def test_put_update_with_attachment_only
810 909 set_tmp_attachments_directory
811 910
812 911 # Delete all fixtured journals, a race condition can occur causing the wrong
813 912 # journal to get fetched in the next find.
814 913 Journal.delete_all
815 914
816 915 # anonymous user
817 916 put :update,
818 917 :id => 1,
819 918 :notes => '',
820 919 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
821 920 assert_redirected_to :action => 'show', :id => '1'
822 921 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
823 922 assert j.notes.blank?
824 923 assert_equal 1, j.details.size
825 924 assert_equal 'testfile.txt', j.details.first.value
826 925 assert_equal User.anonymous, j.user
827 926
828 927 mail = ActionMailer::Base.deliveries.last
829 928 assert mail.body.include?('testfile.txt')
830 929 end
831 930
832 931 def test_put_update_with_attachment_that_fails_to_save
833 932 set_tmp_attachments_directory
834 933
835 934 # Delete all fixtured journals, a race condition can occur causing the wrong
836 935 # journal to get fetched in the next find.
837 936 Journal.delete_all
838 937
839 938 # Mock out the unsaved attachment
840 939 Attachment.any_instance.stubs(:create).returns(Attachment.new)
841 940
842 941 # anonymous user
843 942 put :update,
844 943 :id => 1,
845 944 :notes => '',
846 945 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
847 946 assert_redirected_to :action => 'show', :id => '1'
848 947 assert_equal '1 file(s) could not be saved.', flash[:warning]
849 948
850 949 end if Object.const_defined?(:Mocha)
851 950
852 951 def test_put_update_with_no_change
853 952 issue = Issue.find(1)
854 953 issue.journals.clear
855 954 ActionMailer::Base.deliveries.clear
856 955
857 956 put :update,
858 957 :id => 1,
859 958 :notes => ''
860 959 assert_redirected_to :action => 'show', :id => '1'
861 960
862 961 issue.reload
863 962 assert issue.journals.empty?
864 963 # No email should be sent
865 964 assert ActionMailer::Base.deliveries.empty?
866 965 end
867 966
868 967 def test_put_update_should_send_a_notification
869 968 @request.session[:user_id] = 2
870 969 ActionMailer::Base.deliveries.clear
871 970 issue = Issue.find(1)
872 971 old_subject = issue.subject
873 972 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
874 973
875 974 put :update, :id => 1, :issue => {:subject => new_subject,
876 975 :priority_id => '6',
877 976 :category_id => '1' # no change
878 977 }
879 978 assert_equal 1, ActionMailer::Base.deliveries.size
880 979 end
881 980
882 981 def test_put_update_with_invalid_spent_time
883 982 @request.session[:user_id] = 2
884 983 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
885 984
886 985 assert_no_difference('Journal.count') do
887 986 put :update,
888 987 :id => 1,
889 988 :notes => notes,
890 989 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
891 990 end
892 991 assert_response :success
893 992 assert_template 'edit'
894 993
895 994 assert_tag :textarea, :attributes => { :name => 'notes' },
896 995 :content => notes
897 996 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
898 997 end
899 998
900 999 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
901 1000 issue = Issue.find(2)
902 1001 @request.session[:user_id] = 2
903 1002
904 1003 put :update,
905 1004 :id => issue.id,
906 1005 :issue => {
907 1006 :fixed_version_id => 4
908 1007 }
909 1008
910 1009 assert_response :redirect
911 1010 issue.reload
912 1011 assert_equal 4, issue.fixed_version_id
913 1012 assert_not_equal issue.project_id, issue.fixed_version.project_id
914 1013 end
915 1014
916 1015 def test_put_update_should_redirect_back_using_the_back_url_parameter
917 1016 issue = Issue.find(2)
918 1017 @request.session[:user_id] = 2
919 1018
920 1019 put :update,
921 1020 :id => issue.id,
922 1021 :issue => {
923 1022 :fixed_version_id => 4
924 1023 },
925 1024 :back_url => '/issues'
926 1025
927 1026 assert_response :redirect
928 1027 assert_redirected_to '/issues'
929 1028 end
930 1029
931 1030 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
932 1031 issue = Issue.find(2)
933 1032 @request.session[:user_id] = 2
934 1033
935 1034 put :update,
936 1035 :id => issue.id,
937 1036 :issue => {
938 1037 :fixed_version_id => 4
939 1038 },
940 1039 :back_url => 'http://google.com'
941 1040
942 1041 assert_response :redirect
943 1042 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
944 1043 end
945 1044
946 1045 def test_get_bulk_edit
947 1046 @request.session[:user_id] = 2
948 1047 get :bulk_edit, :ids => [1, 2]
949 1048 assert_response :success
950 1049 assert_template 'bulk_edit'
951 1050
952 1051 # Project specific custom field, date type
953 1052 field = CustomField.find(9)
954 1053 assert !field.is_for_all?
955 1054 assert_equal 'date', field.field_format
956 1055 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
957 1056
958 1057 # System wide custom field
959 1058 assert CustomField.find(1).is_for_all?
960 1059 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
961 1060 end
962 1061
963 1062 def test_get_bulk_edit_on_different_projects
964 1063 @request.session[:user_id] = 2
965 1064 get :bulk_edit, :ids => [1, 2, 6]
966 1065 assert_response :success
967 1066 assert_template 'bulk_edit'
968 1067
969 1068 # Project specific custom field, date type
970 1069 field = CustomField.find(9)
971 1070 assert !field.is_for_all?
972 1071 assert !field.project_ids.include?(Issue.find(6).project_id)
973 1072 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
974 1073 end
975 1074
976 1075 def test_bulk_update
977 1076 @request.session[:user_id] = 2
978 1077 # update issues priority
979 1078 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
980 1079 :issue => {:priority_id => 7,
981 1080 :assigned_to_id => '',
982 1081 :custom_field_values => {'2' => ''}}
983 1082
984 1083 assert_response 302
985 1084 # check that the issues were updated
986 1085 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
987 1086
988 1087 issue = Issue.find(1)
989 1088 journal = issue.journals.find(:first, :order => 'created_on DESC')
990 1089 assert_equal '125', issue.custom_value_for(2).value
991 1090 assert_equal 'Bulk editing', journal.notes
992 1091 assert_equal 1, journal.details.size
993 1092 end
994 1093
995 1094 def test_bulk_update_on_different_projects
996 1095 @request.session[:user_id] = 2
997 1096 # update issues priority
998 1097 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
999 1098 :issue => {:priority_id => 7,
1000 1099 :assigned_to_id => '',
1001 1100 :custom_field_values => {'2' => ''}}
1002 1101
1003 1102 assert_response 302
1004 1103 # check that the issues were updated
1005 1104 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
1006 1105
1007 1106 issue = Issue.find(1)
1008 1107 journal = issue.journals.find(:first, :order => 'created_on DESC')
1009 1108 assert_equal '125', issue.custom_value_for(2).value
1010 1109 assert_equal 'Bulk editing', journal.notes
1011 1110 assert_equal 1, journal.details.size
1012 1111 end
1013 1112
1014 1113 def test_bulk_update_on_different_projects_without_rights
1015 1114 @request.session[:user_id] = 3
1016 1115 user = User.find(3)
1017 1116 action = { :controller => "issues", :action => "bulk_update" }
1018 1117 assert user.allowed_to?(action, Issue.find(1).project)
1019 1118 assert ! user.allowed_to?(action, Issue.find(6).project)
1020 1119 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
1021 1120 :issue => {:priority_id => 7,
1022 1121 :assigned_to_id => '',
1023 1122 :custom_field_values => {'2' => ''}}
1024 1123 assert_response 403
1025 1124 assert_not_equal "Bulk should fail", Journal.last.notes
1026 1125 end
1027 1126
1028 1127 def test_bullk_update_should_send_a_notification
1029 1128 @request.session[:user_id] = 2
1030 1129 ActionMailer::Base.deliveries.clear
1031 1130 post(:bulk_update,
1032 1131 {
1033 1132 :ids => [1, 2],
1034 1133 :notes => 'Bulk editing',
1035 1134 :issue => {
1036 1135 :priority_id => 7,
1037 1136 :assigned_to_id => '',
1038 1137 :custom_field_values => {'2' => ''}
1039 1138 }
1040 1139 })
1041 1140
1042 1141 assert_response 302
1043 1142 assert_equal 2, ActionMailer::Base.deliveries.size
1044 1143 end
1045 1144
1046 1145 def test_bulk_update_status
1047 1146 @request.session[:user_id] = 2
1048 1147 # update issues priority
1049 1148 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
1050 1149 :issue => {:priority_id => '',
1051 1150 :assigned_to_id => '',
1052 1151 :status_id => '5'}
1053 1152
1054 1153 assert_response 302
1055 1154 issue = Issue.find(1)
1056 1155 assert issue.closed?
1057 1156 end
1058 1157
1059 1158 def test_bulk_update_custom_field
1060 1159 @request.session[:user_id] = 2
1061 1160 # update issues priority
1062 1161 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
1063 1162 :issue => {:priority_id => '',
1064 1163 :assigned_to_id => '',
1065 1164 :custom_field_values => {'2' => '777'}}
1066 1165
1067 1166 assert_response 302
1068 1167
1069 1168 issue = Issue.find(1)
1070 1169 journal = issue.journals.find(:first, :order => 'created_on DESC')
1071 1170 assert_equal '777', issue.custom_value_for(2).value
1072 1171 assert_equal 1, journal.details.size
1073 1172 assert_equal '125', journal.details.first.old_value
1074 1173 assert_equal '777', journal.details.first.value
1075 1174 end
1076 1175
1077 1176 def test_bulk_update_unassign
1078 1177 assert_not_nil Issue.find(2).assigned_to
1079 1178 @request.session[:user_id] = 2
1080 1179 # unassign issues
1081 1180 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1082 1181 assert_response 302
1083 1182 # check that the issues were updated
1084 1183 assert_nil Issue.find(2).assigned_to
1085 1184 end
1086 1185
1087 1186 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1088 1187 @request.session[:user_id] = 2
1089 1188
1090 1189 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1091 1190
1092 1191 assert_response :redirect
1093 1192 issues = Issue.find([1,2])
1094 1193 issues.each do |issue|
1095 1194 assert_equal 4, issue.fixed_version_id
1096 1195 assert_not_equal issue.project_id, issue.fixed_version.project_id
1097 1196 end
1098 1197 end
1099 1198
1100 1199 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1101 1200 @request.session[:user_id] = 2
1102 1201 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1103 1202
1104 1203 assert_response :redirect
1105 1204 assert_redirected_to '/issues'
1106 1205 end
1107 1206
1108 1207 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1109 1208 @request.session[:user_id] = 2
1110 1209 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1111 1210
1112 1211 assert_response :redirect
1113 1212 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1114 1213 end
1115 1214
1116 1215 def test_destroy_issue_with_no_time_entries
1117 1216 assert_nil TimeEntry.find_by_issue_id(2)
1118 1217 @request.session[:user_id] = 2
1119 1218 post :destroy, :id => 2
1120 1219 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1121 1220 assert_nil Issue.find_by_id(2)
1122 1221 end
1123 1222
1124 1223 def test_destroy_issues_with_time_entries
1125 1224 @request.session[:user_id] = 2
1126 1225 post :destroy, :ids => [1, 3]
1127 1226 assert_response :success
1128 1227 assert_template 'destroy'
1129 1228 assert_not_nil assigns(:hours)
1130 1229 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1131 1230 end
1132 1231
1133 1232 def test_destroy_issues_and_destroy_time_entries
1134 1233 @request.session[:user_id] = 2
1135 1234 post :destroy, :ids => [1, 3], :todo => 'destroy'
1136 1235 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1137 1236 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1138 1237 assert_nil TimeEntry.find_by_id([1, 2])
1139 1238 end
1140 1239
1141 1240 def test_destroy_issues_and_assign_time_entries_to_project
1142 1241 @request.session[:user_id] = 2
1143 1242 post :destroy, :ids => [1, 3], :todo => 'nullify'
1144 1243 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1145 1244 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1146 1245 assert_nil TimeEntry.find(1).issue_id
1147 1246 assert_nil TimeEntry.find(2).issue_id
1148 1247 end
1149 1248
1150 1249 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1151 1250 @request.session[:user_id] = 2
1152 1251 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1153 1252 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1154 1253 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1155 1254 assert_equal 2, TimeEntry.find(1).issue_id
1156 1255 assert_equal 2, TimeEntry.find(2).issue_id
1157 1256 end
1158 1257
1159 1258 def test_destroy_issues_from_different_projects
1160 1259 @request.session[:user_id] = 2
1161 1260 post :destroy, :ids => [1, 2, 6], :todo => 'destroy'
1162 1261 assert_redirected_to :controller => 'issues', :action => 'index'
1163 1262 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
1164 1263 end
1165 1264
1166 1265 def test_default_search_scope
1167 1266 get :index
1168 1267 assert_tag :div, :attributes => {:id => 'quick-search'},
1169 1268 :child => {:tag => 'form',
1170 1269 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1171 1270 end
1172 1271 end
General Comments 0
You need to be logged in to leave comments. Login now