##// END OF EJS Templates
Allow additional workflow transitions for issue author and assignee (#2732)....
Jean-Philippe Lang -
r4775:4b096e9a5676
parent child
Show More
@@ -0,0 +1,40
1 <table class="list transitions-<%= name %>">
2 <thead>
3 <tr>
4 <th align="left">
5 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input')",
6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
7 <%=l(:label_current_status)%>
8 </th>
9 <th align="center" colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
10 </tr>
11 <tr>
12 <td></td>
13 <% for new_status in @statuses %>
14 <td width="<%= 75 / @statuses.size %>%" align="center">
15 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')",
16 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
17 <%=h new_status.name %>
18 </td>
19 <% end %>
20 </tr>
21 </thead>
22 <tbody>
23 <% for old_status in @statuses %>
24 <tr class="<%= cycle("odd", "even") %>">
25 <td>
26 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.id}')",
27 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
28
29 <%=h old_status.name %>
30 </td>
31 <% for new_status in @statuses -%>
32 <td align="center">
33 <%= check_box_tag "issue_status[#{ old_status.id }][#{new_status.id}][]", name, workflows.detect {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id},
34 :class => "old-status-#{old_status.id} new-status-#{new_status.id}" %>
35 </td>
36 <% end -%>
37 </tr>
38 <% end %>
39 </tbody>
40 </table> No newline at end of file
@@ -0,0 +1,13
1 class AddWorkflowsAssigneeAndAuthor < ActiveRecord::Migration
2 def self.up
3 add_column :workflows, :assignee, :boolean, :null => false, :default => false
4 add_column :workflows, :author, :boolean, :null => false, :default => false
5 Workflow.update_all("assignee = #{Workflow.connection.quoted_false}")
6 Workflow.update_all("author = #{Workflow.connection.quoted_false}")
7 end
8
9 def self.down
10 remove_column :workflows, :assignee
11 remove_column :workflows, :author
12 end
13 end
@@ -1,91 +1,102
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 WorkflowsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22 before_filter :find_roles
23 23 before_filter :find_trackers
24 24
25 25 def index
26 26 @workflow_counts = Workflow.count_by_tracker_and_role
27 27 end
28 28
29 29 def edit
30 30 @role = Role.find_by_id(params[:role_id])
31 31 @tracker = Tracker.find_by_id(params[:tracker_id])
32 32
33 33 if request.post?
34 34 Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
35 (params[:issue_status] || []).each { |old, news|
36 news.each { |new|
37 @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new)
35 (params[:issue_status] || []).each { |status_id, transitions|
36 transitions.each { |new_status_id, options|
37 author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
38 assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
39 @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
38 40 }
39 41 }
40 42 if @role.save
41 43 flash[:notice] = l(:notice_successful_update)
42 44 redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
45 return
43 46 end
44 47 end
45 48
46 49 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
47 50 if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
48 51 @statuses = @tracker.issue_statuses
49 52 end
50 53 @statuses ||= IssueStatus.find(:all, :order => 'position')
54
55 if @tracker && @role && @statuses.any?
56 workflows = Workflow.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id})
57 @workflows = {}
58 @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
59 @workflows['author'] = workflows.select {|w| w.author}
60 @workflows['assignee'] = workflows.select {|w| w.assignee}
61 end
51 62 end
52 63
53 64 def copy
54 65
55 66 if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
56 67 @source_tracker = nil
57 68 else
58 69 @source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
59 70 end
60 71 if params[:source_role_id].blank? || params[:source_role_id] == 'any'
61 72 @source_role = nil
62 73 else
63 74 @source_role = Role.find_by_id(params[:source_role_id].to_i)
64 75 end
65 76
66 77 @target_trackers = params[:target_tracker_ids].blank? ? nil : Tracker.find_all_by_id(params[:target_tracker_ids])
67 78 @target_roles = params[:target_role_ids].blank? ? nil : Role.find_all_by_id(params[:target_role_ids])
68 79
69 80 if request.post?
70 81 if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
71 82 flash.now[:error] = l(:error_workflow_copy_source)
72 83 elsif @target_trackers.nil? || @target_roles.nil?
73 84 flash.now[:error] = l(:error_workflow_copy_target)
74 85 else
75 86 Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
76 87 flash[:notice] = l(:notice_successful_update)
77 88 redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role
78 89 end
79 90 end
80 91 end
81 92
82 93 private
83 94
84 95 def find_roles
85 96 @roles = Role.find(:all, :order => 'builtin, position')
86 97 end
87 98
88 99 def find_trackers
89 100 @trackers = Tracker.find(:all, :order => 'position')
90 101 end
91 102 end
@@ -1,876 +1,881
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 include Redmine::SafeAttributes
20 20
21 21 belongs_to :project
22 22 belongs_to :tracker
23 23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 25 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
26 26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29 29
30 30 has_many :journals, :as => :journalized, :dependent => :destroy
31 31 has_many :time_entries, :dependent => :delete_all
32 32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33 33
34 34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 36
37 37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 38 acts_as_attachable :after_remove => :attachment_removed
39 39 acts_as_customizable
40 40 acts_as_watchable
41 41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 42 :include => [:project, :journals],
43 43 # sort by id so that limited eager loading doesn't break with postgresql
44 44 :order_column => "#{table_name}.id"
45 45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48 48
49 49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 50 :author_key => :author_id
51 51
52 52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53 53
54 54 attr_reader :current_journal
55 55
56 56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57 57
58 58 validates_length_of :subject, :maximum => 255
59 59 validates_inclusion_of :done_ratio, :in => 0..100
60 60 validates_numericality_of :estimated_hours, :allow_nil => true
61 61
62 62 named_scope :visible, lambda {|*args| { :include => :project,
63 63 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
64 64
65 65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
66 66
67 67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
68 68 named_scope :with_limit, lambda { |limit| { :limit => limit} }
69 69 named_scope :on_active_project, :include => [:status, :project, :tracker],
70 70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
71 71 named_scope :for_gantt, lambda {
72 72 {
73 73 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version]
74 74 }
75 75 }
76 76
77 77 named_scope :without_version, lambda {
78 78 {
79 79 :conditions => { :fixed_version_id => nil}
80 80 }
81 81 }
82 82
83 83 named_scope :with_query, lambda {|query|
84 84 {
85 85 :conditions => Query.merge_conditions(query.statement)
86 86 }
87 87 }
88 88
89 89 before_create :default_assign
90 90 before_save :close_duplicates, :update_done_ratio_from_issue_status
91 91 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
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 'tracker_id',
219 219 'status_id',
220 220 'parent_issue_id',
221 221 'category_id',
222 222 'assigned_to_id',
223 223 'priority_id',
224 224 'fixed_version_id',
225 225 'subject',
226 226 'description',
227 227 'start_date',
228 228 'due_date',
229 229 'done_ratio',
230 230 'estimated_hours',
231 231 'custom_field_values',
232 232 'custom_fields',
233 233 'lock_version',
234 234 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
235 235
236 236 safe_attributes 'status_id',
237 237 'assigned_to_id',
238 238 'fixed_version_id',
239 239 'done_ratio',
240 240 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
241 241
242 242 # Safely sets attributes
243 243 # Should be called from controllers instead of #attributes=
244 244 # attr_accessible is too rough because we still want things like
245 245 # Issue.new(:project => foo) to work
246 246 # TODO: move workflow/permission checks from controllers to here
247 247 def safe_attributes=(attrs, user=User.current)
248 248 return unless attrs.is_a?(Hash)
249 249
250 250 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
251 251 attrs = delete_unsafe_attributes(attrs, user)
252 252 return if attrs.empty?
253 253
254 254 # Tracker must be set before since new_statuses_allowed_to depends on it.
255 255 if t = attrs.delete('tracker_id')
256 256 self.tracker_id = t
257 257 end
258 258
259 259 if attrs['status_id']
260 260 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
261 261 attrs.delete('status_id')
262 262 end
263 263 end
264 264
265 265 unless leaf?
266 266 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
267 267 end
268 268
269 269 if attrs.has_key?('parent_issue_id')
270 270 if !user.allowed_to?(:manage_subtasks, project)
271 271 attrs.delete('parent_issue_id')
272 272 elsif !attrs['parent_issue_id'].blank?
273 273 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
274 274 end
275 275 end
276 276
277 277 self.attributes = attrs
278 278 end
279 279
280 280 def done_ratio
281 281 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
282 282 status.default_done_ratio
283 283 else
284 284 read_attribute(:done_ratio)
285 285 end
286 286 end
287 287
288 288 def self.use_status_for_done_ratio?
289 289 Setting.issue_done_ratio == 'issue_status'
290 290 end
291 291
292 292 def self.use_field_for_done_ratio?
293 293 Setting.issue_done_ratio == 'issue_field'
294 294 end
295 295
296 296 def validate
297 297 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
298 298 errors.add :due_date, :not_a_date
299 299 end
300 300
301 301 if self.due_date and self.start_date and self.due_date < self.start_date
302 302 errors.add :due_date, :greater_than_start_date
303 303 end
304 304
305 305 if start_date && soonest_start && start_date < soonest_start
306 306 errors.add :start_date, :invalid
307 307 end
308 308
309 309 if fixed_version
310 310 if !assignable_versions.include?(fixed_version)
311 311 errors.add :fixed_version_id, :inclusion
312 312 elsif reopened? && fixed_version.closed?
313 313 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
314 314 end
315 315 end
316 316
317 317 # Checks that the issue can not be added/moved to a disabled tracker
318 318 if project && (tracker_id_changed? || project_id_changed?)
319 319 unless project.trackers.include?(tracker)
320 320 errors.add :tracker_id, :inclusion
321 321 end
322 322 end
323 323
324 324 # Checks parent issue assignment
325 325 if @parent_issue
326 326 if @parent_issue.project_id != project_id
327 327 errors.add :parent_issue_id, :not_same_project
328 328 elsif !new_record?
329 329 # moving an existing issue
330 330 if @parent_issue.root_id != root_id
331 331 # we can always move to another tree
332 332 elsif move_possible?(@parent_issue)
333 333 # move accepted inside tree
334 334 else
335 335 errors.add :parent_issue_id, :not_a_valid_parent
336 336 end
337 337 end
338 338 end
339 339 end
340 340
341 341 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
342 342 # even if the user turns off the setting later
343 343 def update_done_ratio_from_issue_status
344 344 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
345 345 self.done_ratio = status.default_done_ratio
346 346 end
347 347 end
348 348
349 349 def init_journal(user, notes = "")
350 350 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
351 351 @issue_before_change = self.clone
352 352 @issue_before_change.status = self.status
353 353 @custom_values_before_change = {}
354 354 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
355 355 # Make sure updated_on is updated when adding a note.
356 356 updated_on_will_change!
357 357 @current_journal
358 358 end
359 359
360 360 # Return true if the issue is closed, otherwise false
361 361 def closed?
362 362 self.status.is_closed?
363 363 end
364 364
365 365 # Return true if the issue is being reopened
366 366 def reopened?
367 367 if !new_record? && status_id_changed?
368 368 status_was = IssueStatus.find_by_id(status_id_was)
369 369 status_new = IssueStatus.find_by_id(status_id)
370 370 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
371 371 return true
372 372 end
373 373 end
374 374 false
375 375 end
376 376
377 377 # Return true if the issue is being closed
378 378 def closing?
379 379 if !new_record? && status_id_changed?
380 380 status_was = IssueStatus.find_by_id(status_id_was)
381 381 status_new = IssueStatus.find_by_id(status_id)
382 382 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
383 383 return true
384 384 end
385 385 end
386 386 false
387 387 end
388 388
389 389 # Returns true if the issue is overdue
390 390 def overdue?
391 391 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
392 392 end
393 393
394 394 # Is the amount of work done less than it should for the due date
395 395 def behind_schedule?
396 396 return false if start_date.nil? || due_date.nil?
397 397 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
398 398 return done_date <= Date.today
399 399 end
400 400
401 401 # Does this issue have children?
402 402 def children?
403 403 !leaf?
404 404 end
405 405
406 406 # Users the issue can be assigned to
407 407 def assignable_users
408 408 users = project.assignable_users
409 409 users << author if author
410 410 users.uniq.sort
411 411 end
412 412
413 413 # Versions that the issue can be assigned to
414 414 def assignable_versions
415 415 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
416 416 end
417 417
418 418 # Returns true if this issue is blocked by another issue that is still open
419 419 def blocked?
420 420 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
421 421 end
422 422
423 423 # Returns an array of status that user is able to apply
424 424 def new_statuses_allowed_to(user, include_default=false)
425 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
425 statuses = status.find_new_statuses_allowed_to(
426 user.roles_for_project(project),
427 tracker,
428 author == user,
429 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
430 )
426 431 statuses << status unless statuses.empty?
427 432 statuses << IssueStatus.default if include_default
428 433 statuses = statuses.uniq.sort
429 434 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
430 435 end
431 436
432 437 # Returns the mail adresses of users that should be notified
433 438 def recipients
434 439 notified = project.notified_users
435 440 # Author and assignee are always notified unless they have been
436 441 # locked or don't want to be notified
437 442 notified << author if author && author.active? && author.notify_about?(self)
438 443 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
439 444 notified.uniq!
440 445 # Remove users that can not view the issue
441 446 notified.reject! {|user| !visible?(user)}
442 447 notified.collect(&:mail)
443 448 end
444 449
445 450 # Returns the total number of hours spent on this issue and its descendants
446 451 #
447 452 # Example:
448 453 # spent_hours => 0.0
449 454 # spent_hours => 50.2
450 455 def spent_hours
451 456 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
452 457 end
453 458
454 459 def relations
455 460 (relations_from + relations_to).sort
456 461 end
457 462
458 463 def all_dependent_issues(except=nil)
459 464 except ||= self
460 465 dependencies = []
461 466 relations_from.each do |relation|
462 467 if relation.issue_to && relation.issue_to != except
463 468 dependencies << relation.issue_to
464 469 dependencies += relation.issue_to.all_dependent_issues(except)
465 470 end
466 471 end
467 472 dependencies
468 473 end
469 474
470 475 # Returns an array of issues that duplicate this one
471 476 def duplicates
472 477 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
473 478 end
474 479
475 480 # Returns the due date or the target due date if any
476 481 # Used on gantt chart
477 482 def due_before
478 483 due_date || (fixed_version ? fixed_version.effective_date : nil)
479 484 end
480 485
481 486 # Returns the time scheduled for this issue.
482 487 #
483 488 # Example:
484 489 # Start Date: 2/26/09, End Date: 3/04/09
485 490 # duration => 6
486 491 def duration
487 492 (start_date && due_date) ? due_date - start_date : 0
488 493 end
489 494
490 495 def soonest_start
491 496 @soonest_start ||= (
492 497 relations_to.collect{|relation| relation.successor_soonest_start} +
493 498 ancestors.collect(&:soonest_start)
494 499 ).compact.max
495 500 end
496 501
497 502 def reschedule_after(date)
498 503 return if date.nil?
499 504 if leaf?
500 505 if start_date.nil? || start_date < date
501 506 self.start_date, self.due_date = date, date + duration
502 507 save
503 508 end
504 509 else
505 510 leaves.each do |leaf|
506 511 leaf.reschedule_after(date)
507 512 end
508 513 end
509 514 end
510 515
511 516 def <=>(issue)
512 517 if issue.nil?
513 518 -1
514 519 elsif root_id != issue.root_id
515 520 (root_id || 0) <=> (issue.root_id || 0)
516 521 else
517 522 (lft || 0) <=> (issue.lft || 0)
518 523 end
519 524 end
520 525
521 526 def to_s
522 527 "#{tracker} ##{id}: #{subject}"
523 528 end
524 529
525 530 # Returns a string of css classes that apply to the issue
526 531 def css_classes
527 532 s = "issue status-#{status.position} priority-#{priority.position}"
528 533 s << ' closed' if closed?
529 534 s << ' overdue' if overdue?
530 535 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
531 536 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
532 537 s
533 538 end
534 539
535 540 # Saves an issue, time_entry, attachments, and a journal from the parameters
536 541 # Returns false if save fails
537 542 def save_issue_with_child_records(params, existing_time_entry=nil)
538 543 Issue.transaction do
539 544 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
540 545 @time_entry = existing_time_entry || TimeEntry.new
541 546 @time_entry.project = project
542 547 @time_entry.issue = self
543 548 @time_entry.user = User.current
544 549 @time_entry.spent_on = Date.today
545 550 @time_entry.attributes = params[:time_entry]
546 551 self.time_entries << @time_entry
547 552 end
548 553
549 554 if valid?
550 555 attachments = Attachment.attach_files(self, params[:attachments])
551 556
552 557 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
553 558 # TODO: Rename hook
554 559 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
555 560 begin
556 561 if save
557 562 # TODO: Rename hook
558 563 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
559 564 else
560 565 raise ActiveRecord::Rollback
561 566 end
562 567 rescue ActiveRecord::StaleObjectError
563 568 attachments[:files].each(&:destroy)
564 569 errors.add_to_base l(:notice_locking_conflict)
565 570 raise ActiveRecord::Rollback
566 571 end
567 572 end
568 573 end
569 574 end
570 575
571 576 # Unassigns issues from +version+ if it's no longer shared with issue's project
572 577 def self.update_versions_from_sharing_change(version)
573 578 # Update issues assigned to the version
574 579 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
575 580 end
576 581
577 582 # Unassigns issues from versions that are no longer shared
578 583 # after +project+ was moved
579 584 def self.update_versions_from_hierarchy_change(project)
580 585 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
581 586 # Update issues of the moved projects and issues assigned to a version of a moved project
582 587 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
583 588 end
584 589
585 590 def parent_issue_id=(arg)
586 591 parent_issue_id = arg.blank? ? nil : arg.to_i
587 592 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
588 593 @parent_issue.id
589 594 else
590 595 @parent_issue = nil
591 596 nil
592 597 end
593 598 end
594 599
595 600 def parent_issue_id
596 601 if instance_variable_defined? :@parent_issue
597 602 @parent_issue.nil? ? nil : @parent_issue.id
598 603 else
599 604 parent_id
600 605 end
601 606 end
602 607
603 608 # Extracted from the ReportsController.
604 609 def self.by_tracker(project)
605 610 count_and_group_by(:project => project,
606 611 :field => 'tracker_id',
607 612 :joins => Tracker.table_name)
608 613 end
609 614
610 615 def self.by_version(project)
611 616 count_and_group_by(:project => project,
612 617 :field => 'fixed_version_id',
613 618 :joins => Version.table_name)
614 619 end
615 620
616 621 def self.by_priority(project)
617 622 count_and_group_by(:project => project,
618 623 :field => 'priority_id',
619 624 :joins => IssuePriority.table_name)
620 625 end
621 626
622 627 def self.by_category(project)
623 628 count_and_group_by(:project => project,
624 629 :field => 'category_id',
625 630 :joins => IssueCategory.table_name)
626 631 end
627 632
628 633 def self.by_assigned_to(project)
629 634 count_and_group_by(:project => project,
630 635 :field => 'assigned_to_id',
631 636 :joins => User.table_name)
632 637 end
633 638
634 639 def self.by_author(project)
635 640 count_and_group_by(:project => project,
636 641 :field => 'author_id',
637 642 :joins => User.table_name)
638 643 end
639 644
640 645 def self.by_subproject(project)
641 646 ActiveRecord::Base.connection.select_all("select s.id as status_id,
642 647 s.is_closed as closed,
643 648 i.project_id as project_id,
644 649 count(i.id) as total
645 650 from
646 651 #{Issue.table_name} i, #{IssueStatus.table_name} s
647 652 where
648 653 i.status_id=s.id
649 654 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
650 655 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
651 656 end
652 657 # End ReportsController extraction
653 658
654 659 # Returns an array of projects that current user can move issues to
655 660 def self.allowed_target_projects_on_move
656 661 projects = []
657 662 if User.current.admin?
658 663 # admin is allowed to move issues to any active (visible) project
659 664 projects = Project.visible.all
660 665 elsif User.current.logged?
661 666 if Role.non_member.allowed_to?(:move_issues)
662 667 projects = Project.visible.all
663 668 else
664 669 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
665 670 end
666 671 end
667 672 projects
668 673 end
669 674
670 675 private
671 676
672 677 def update_nested_set_attributes
673 678 if root_id.nil?
674 679 # issue was just created
675 680 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
676 681 set_default_left_and_right
677 682 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
678 683 if @parent_issue
679 684 move_to_child_of(@parent_issue)
680 685 end
681 686 reload
682 687 elsif parent_issue_id != parent_id
683 688 former_parent_id = parent_id
684 689 # moving an existing issue
685 690 if @parent_issue && @parent_issue.root_id == root_id
686 691 # inside the same tree
687 692 move_to_child_of(@parent_issue)
688 693 else
689 694 # to another tree
690 695 unless root?
691 696 move_to_right_of(root)
692 697 reload
693 698 end
694 699 old_root_id = root_id
695 700 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
696 701 target_maxright = nested_set_scope.maximum(right_column_name) || 0
697 702 offset = target_maxright + 1 - lft
698 703 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
699 704 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
700 705 self[left_column_name] = lft + offset
701 706 self[right_column_name] = rgt + offset
702 707 if @parent_issue
703 708 move_to_child_of(@parent_issue)
704 709 end
705 710 end
706 711 reload
707 712 # delete invalid relations of all descendants
708 713 self_and_descendants.each do |issue|
709 714 issue.relations.each do |relation|
710 715 relation.destroy unless relation.valid?
711 716 end
712 717 end
713 718 # update former parent
714 719 recalculate_attributes_for(former_parent_id) if former_parent_id
715 720 end
716 721 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
717 722 end
718 723
719 724 def update_parent_attributes
720 725 recalculate_attributes_for(parent_id) if parent_id
721 726 end
722 727
723 728 def recalculate_attributes_for(issue_id)
724 729 if issue_id && p = Issue.find_by_id(issue_id)
725 730 # priority = highest priority of children
726 731 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
727 732 p.priority = IssuePriority.find_by_position(priority_position)
728 733 end
729 734
730 735 # start/due dates = lowest/highest dates of children
731 736 p.start_date = p.children.minimum(:start_date)
732 737 p.due_date = p.children.maximum(:due_date)
733 738 if p.start_date && p.due_date && p.due_date < p.start_date
734 739 p.start_date, p.due_date = p.due_date, p.start_date
735 740 end
736 741
737 742 # done ratio = weighted average ratio of leaves
738 743 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
739 744 leaves_count = p.leaves.count
740 745 if leaves_count > 0
741 746 average = p.leaves.average(:estimated_hours).to_f
742 747 if average == 0
743 748 average = 1
744 749 end
745 750 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
746 751 progress = done / (average * leaves_count)
747 752 p.done_ratio = progress.round
748 753 end
749 754 end
750 755
751 756 # estimate = sum of leaves estimates
752 757 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
753 758 p.estimated_hours = nil if p.estimated_hours == 0.0
754 759
755 760 # ancestors will be recursively updated
756 761 p.save(false)
757 762 end
758 763 end
759 764
760 765 # Update issues so their versions are not pointing to a
761 766 # fixed_version that is not shared with the issue's project
762 767 def self.update_versions(conditions=nil)
763 768 # Only need to update issues with a fixed_version from
764 769 # a different project and that is not systemwide shared
765 770 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
766 771 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
767 772 " AND #{Version.table_name}.sharing <> 'system'",
768 773 conditions),
769 774 :include => [:project, :fixed_version]
770 775 ).each do |issue|
771 776 next if issue.project.nil? || issue.fixed_version.nil?
772 777 unless issue.project.shared_versions.include?(issue.fixed_version)
773 778 issue.init_journal(User.current)
774 779 issue.fixed_version = nil
775 780 issue.save
776 781 end
777 782 end
778 783 end
779 784
780 785 # Callback on attachment deletion
781 786 def attachment_removed(obj)
782 787 journal = init_journal(User.current)
783 788 journal.details << JournalDetail.new(:property => 'attachment',
784 789 :prop_key => obj.id,
785 790 :old_value => obj.filename)
786 791 journal.save
787 792 end
788 793
789 794 # Default assignment based on category
790 795 def default_assign
791 796 if assigned_to.nil? && category && category.assigned_to
792 797 self.assigned_to = category.assigned_to
793 798 end
794 799 end
795 800
796 801 # Updates start/due dates of following issues
797 802 def reschedule_following_issues
798 803 if start_date_changed? || due_date_changed?
799 804 relations_from.each do |relation|
800 805 relation.set_issue_to_dates
801 806 end
802 807 end
803 808 end
804 809
805 810 # Closes duplicates if the issue is being closed
806 811 def close_duplicates
807 812 if closing?
808 813 duplicates.each do |duplicate|
809 814 # Reload is need in case the duplicate was updated by a previous duplicate
810 815 duplicate.reload
811 816 # Don't re-close it if it's already closed
812 817 next if duplicate.closed?
813 818 # Same user and notes
814 819 if @current_journal
815 820 duplicate.init_journal(@current_journal.user, @current_journal.notes)
816 821 end
817 822 duplicate.update_attribute :status, self.status
818 823 end
819 824 end
820 825 end
821 826
822 827 # Saves the changes in a Journal
823 828 # Called after_save
824 829 def create_journal
825 830 if @current_journal
826 831 # attributes changes
827 832 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
828 833 @current_journal.details << JournalDetail.new(:property => 'attr',
829 834 :prop_key => c,
830 835 :old_value => @issue_before_change.send(c),
831 836 :value => send(c)) unless send(c)==@issue_before_change.send(c)
832 837 }
833 838 # custom fields changes
834 839 custom_values.each {|c|
835 840 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
836 841 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
837 842 @current_journal.details << JournalDetail.new(:property => 'cf',
838 843 :prop_key => c.custom_field_id,
839 844 :old_value => @custom_values_before_change[c.custom_field_id],
840 845 :value => c.value)
841 846 }
842 847 @current_journal.save
843 848 # reset current journal
844 849 init_journal @current_journal.user, @current_journal.notes
845 850 end
846 851 end
847 852
848 853 # Query generator for selecting groups of issue counts for a project
849 854 # based on specific criteria
850 855 #
851 856 # Options
852 857 # * project - Project to search in.
853 858 # * field - String. Issue field to key off of in the grouping.
854 859 # * joins - String. The table name to join against.
855 860 def self.count_and_group_by(options)
856 861 project = options.delete(:project)
857 862 select_field = options.delete(:field)
858 863 joins = options.delete(:joins)
859 864
860 865 where = "i.#{select_field}=j.id"
861 866
862 867 ActiveRecord::Base.connection.select_all("select s.id as status_id,
863 868 s.is_closed as closed,
864 869 j.id as #{select_field},
865 870 count(i.id) as total
866 871 from
867 872 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
868 873 where
869 874 i.status_id=s.id
870 875 and #{where}
871 876 and i.project_id=#{project.id}
872 877 group by s.id, s.is_closed, j.id")
873 878 end
874 879
875 880
876 881 end
@@ -1,98 +1,99
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueStatus < ActiveRecord::Base
19 19 before_destroy :check_integrity
20 20 has_many :workflows, :foreign_key => "old_status_id"
21 21 acts_as_list
22 22
23 23 before_destroy :delete_workflows
24 24
25 25 validates_presence_of :name
26 26 validates_uniqueness_of :name
27 27 validates_length_of :name, :maximum => 30
28 28 validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
29 29
30 30 def after_save
31 31 IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
32 32 end
33 33
34 34 # Returns the default status for new issues
35 35 def self.default
36 36 find(:first, :conditions =>["is_default=?", true])
37 37 end
38 38
39 39 # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
40 40 def self.update_issue_done_ratios
41 41 if Issue.use_status_for_done_ratio?
42 42 IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
43 43 Issue.update_all(["done_ratio = ?", status.default_done_ratio],
44 44 ["status_id = ?", status.id])
45 45 end
46 46 end
47 47
48 48 return Issue.use_status_for_done_ratio?
49 49 end
50 50
51 51 # Returns an array of all statuses the given role can switch to
52 52 # Uses association cache when called more than one time
53 def new_statuses_allowed_to(roles, tracker)
53 def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
54 54 if roles && tracker
55 55 role_ids = roles.collect(&:id)
56 new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
56 transitions = workflows.select do |w|
57 role_ids.include?(w.role_id) &&
58 w.tracker_id == tracker.id &&
59 (author || !w.author) &&
60 (assignee || !w.assignee)
61 end
62 transitions.collect{|w| w.new_status}.compact.sort
57 63 else
58 64 []
59 65 end
60 66 end
61 67
62 68 # Same thing as above but uses a database query
63 69 # More efficient than the previous method if called just once
64 def find_new_statuses_allowed_to(roles, tracker)
70 def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
65 71 if roles && tracker
72 conditions = {:role_id => roles.collect(&:id), :tracker_id => tracker.id}
73 conditions[:author] = false unless author
74 conditions[:assignee] = false unless assignee
75
66 76 workflows.find(:all,
67 77 :include => :new_status,
68 :conditions => { :role_id => roles.collect(&:id),
69 :tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
78 :conditions => conditions).collect{|w| w.new_status}.compact.sort
70 79 else
71 80 []
72 81 end
73 82 end
74 83
75 def new_status_allowed_to?(status, roles, tracker)
76 if status && roles && tracker
77 !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
78 else
79 false
80 end
81 end
82
83 84 def <=>(status)
84 85 position <=> status.position
85 86 end
86 87
87 88 def to_s; name end
88 89
89 90 private
90 91 def check_integrity
91 92 raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
92 93 end
93 94
94 95 # Deletes associated workflows
95 96 def delete_workflows
96 97 Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
97 98 end
98 99 end
@@ -1,73 +1,50
1 1 <%= render :partial => 'action_menu' %>
2 2
3 3 <h2><%=l(:label_workflow)%></h2>
4 4
5 5 <p><%=l(:text_workflow_edit)%>:</p>
6 6
7 7 <% form_tag({}, :method => 'get') do %>
8 8 <p>
9 9 <label><%=l(:label_role)%>:</label>
10 10 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %>
11 11
12 12 <label><%=l(:label_tracker)%>:</label>
13 13 <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %>
14 14
15 15 <%= hidden_field_tag 'used_statuses_only', '0' %>
16 16 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
17 17 </p>
18 18 <p>
19 19 <%= submit_tag l(:button_edit), :name => nil %>
20 20 </p>
21 21 <% end %>
22 22
23
24 23 <% if @tracker && @role && @statuses.any? %>
25 24 <% form_tag({}, :id => 'workflow_form' ) do %>
26 25 <%= hidden_field_tag 'tracker_id', @tracker.id %>
27 26 <%= hidden_field_tag 'role_id', @role.id %>
28 27 <div class="autoscroll">
29 <table class="list">
30 <thead>
31 <tr>
32 <th align="left"><%=l(:label_current_status)%></th>
33 <th align="center" colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
34 </tr>
35 <tr>
36 <td></td>
37 <% for new_status in @statuses %>
38 <td width="<%= 75 / @statuses.size %>%" align="center">
39 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.new-status-#{new_status.id}')",
40 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
41 <%= new_status.name %>
42 </td>
43 <% end %>
44 </tr>
45 </thead>
46 <tbody>
47 <% for old_status in @statuses %>
48 <tr class="<%= cycle("odd", "even") %>">
49 <td>
50 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.old-status-#{old_status.id}')",
51 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
28 <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
52 29
53 <%= old_status.name %>
54 </td>
55 <% new_status_ids_allowed = old_status.find_new_statuses_allowed_to([@role], @tracker).collect(&:id) -%>
56 <% for new_status in @statuses -%>
57 <td align="center">
58 <%= check_box_tag "issue_status[#{ old_status.id }][]", new_status.id, new_status_ids_allowed.include?(new_status.id),
59 :class => "old-status-#{old_status.id} new-status-#{new_status.id}" %>
60 </td>
61 <% end -%>
62 </tr>
63 <% end %>
64 </tbody>
65 </table>
30 <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
31 <legend onclick="toggleFieldset(this);">Autorisations supplémentaires lorsque l'utilisateur a créé la demande</legend>
32 <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
33 <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
66 34 </div>
67 <p><%= check_all_links 'workflow_form' %></p>
35 </fieldset>
36 <%= javascript_tag "hideFieldset($('author_workflows'))" unless @workflows['author'].present? %>
68 37
38 <fieldset class="collapsible" style="padding: 0;">
39 <legend onclick="toggleFieldset(this);">Autorisations supplΓ©mentaires lorsque la demande est assignΓ©e Γ  l'utilisateur</legend>
40 <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
41 <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
42 </div>
43 </fieldset>
44 <%= javascript_tag "hideFieldset($('assignee_workflows'))" unless @workflows['assignee'].present? %>
45 </div>
69 46 <%= submit_tag l(:button_save) %>
70 47 <% end %>
71 48 <% end %>
72 49
73 50 <% html_title(l(:label_workflow)) -%>
@@ -1,273 +1,279
1 1 /* redMine - project management software
2 2 Copyright (C) 2006-2008 Jean-Philippe Lang */
3 3
4 4 function checkAll (id, checked) {
5 5 var els = Element.descendants(id);
6 6 for (var i = 0; i < els.length; i++) {
7 7 if (els[i].disabled==false) {
8 8 els[i].checked = checked;
9 9 }
10 10 }
11 11 }
12 12
13 13 function toggleCheckboxesBySelector(selector) {
14 14 boxes = $$(selector);
15 15 var all_checked = true;
16 16 for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
17 17 for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; }
18 18 }
19 19
20 20 function setCheckboxesBySelector(checked, selector) {
21 21 var boxes = $$(selector);
22 22 boxes.each(function(ele) {
23 23 ele.checked = checked;
24 24 });
25 25 }
26 26
27 27 function showAndScrollTo(id, focus) {
28 28 Element.show(id);
29 29 if (focus!=null) { Form.Element.focus(focus); }
30 30 Element.scrollTo(id);
31 31 }
32 32
33 33 function toggleRowGroup(el) {
34 34 var tr = Element.up(el, 'tr');
35 35 var n = Element.next(tr);
36 36 tr.toggleClassName('open');
37 37 while (n != undefined && !n.hasClassName('group')) {
38 38 Element.toggle(n);
39 39 n = Element.next(n);
40 40 }
41 41 }
42 42
43 43 function toggleFieldset(el) {
44 44 var fieldset = Element.up(el, 'fieldset');
45 45 fieldset.toggleClassName('collapsed');
46 46 Effect.toggle(fieldset.down('div'), 'slide', {duration:0.2});
47 47 }
48 48
49 function hideFieldset(el) {
50 var fieldset = Element.up(el, 'fieldset');
51 fieldset.toggleClassName('collapsed');
52 fieldset.down('div').hide();
53 }
54
49 55 var fileFieldCount = 1;
50 56
51 57 function addFileField() {
52 58 if (fileFieldCount >= 10) return false
53 59 fileFieldCount++;
54 60 var f = document.createElement("input");
55 61 f.type = "file";
56 62 f.name = "attachments[" + fileFieldCount + "][file]";
57 63 f.size = 30;
58 64 var d = document.createElement("input");
59 65 d.type = "text";
60 66 d.name = "attachments[" + fileFieldCount + "][description]";
61 67 d.size = 60;
62 68 var dLabel = new Element('label');
63 69 dLabel.addClassName('inline');
64 70 // Pulls the languge value used for Optional Description
65 71 dLabel.update($('attachment_description_label_content').innerHTML)
66 72 p = document.getElementById("attachments_fields");
67 73 p.appendChild(document.createElement("br"));
68 74 p.appendChild(f);
69 75 p.appendChild(dLabel);
70 76 dLabel.appendChild(d);
71 77
72 78 }
73 79
74 80 function showTab(name) {
75 81 var f = $$('div#content .tab-content');
76 82 for(var i=0; i<f.length; i++){
77 83 Element.hide(f[i]);
78 84 }
79 85 var f = $$('div.tabs a');
80 86 for(var i=0; i<f.length; i++){
81 87 Element.removeClassName(f[i], "selected");
82 88 }
83 89 Element.show('tab-content-' + name);
84 90 Element.addClassName('tab-' + name, "selected");
85 91 return false;
86 92 }
87 93
88 94 function moveTabRight(el) {
89 95 var lis = Element.up(el, 'div.tabs').down('ul').childElements();
90 96 var tabsWidth = 0;
91 97 var i;
92 98 for (i=0; i<lis.length; i++) {
93 99 if (lis[i].visible()) {
94 100 tabsWidth += lis[i].getWidth() + 6;
95 101 }
96 102 }
97 103 if (tabsWidth < Element.up(el, 'div.tabs').getWidth() - 60) {
98 104 return;
99 105 }
100 106 i=0;
101 107 while (i<lis.length && !lis[i].visible()) {
102 108 i++;
103 109 }
104 110 lis[i].hide();
105 111 }
106 112
107 113 function moveTabLeft(el) {
108 114 var lis = Element.up(el, 'div.tabs').down('ul').childElements();
109 115 var i = 0;
110 116 while (i<lis.length && !lis[i].visible()) {
111 117 i++;
112 118 }
113 119 if (i>0) {
114 120 lis[i-1].show();
115 121 }
116 122 }
117 123
118 124 function displayTabsButtons() {
119 125 var lis;
120 126 var tabsWidth = 0;
121 127 var i;
122 128 $$('div.tabs').each(function(el) {
123 129 lis = el.down('ul').childElements();
124 130 for (i=0; i<lis.length; i++) {
125 131 if (lis[i].visible()) {
126 132 tabsWidth += lis[i].getWidth() + 6;
127 133 }
128 134 }
129 135 if ((tabsWidth < el.getWidth() - 60) && (lis[0].visible())) {
130 136 el.down('div.tabs-buttons').hide();
131 137 } else {
132 138 el.down('div.tabs-buttons').show();
133 139 }
134 140 });
135 141 }
136 142
137 143 function setPredecessorFieldsVisibility() {
138 144 relationType = $('relation_relation_type');
139 145 if (relationType && (relationType.value == "precedes" || relationType.value == "follows")) {
140 146 Element.show('predecessor_fields');
141 147 } else {
142 148 Element.hide('predecessor_fields');
143 149 }
144 150 }
145 151
146 152 function promptToRemote(text, param, url) {
147 153 value = prompt(text + ':');
148 154 if (value) {
149 155 new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true});
150 156 return false;
151 157 }
152 158 }
153 159
154 160 function collapseScmEntry(id) {
155 161 var els = document.getElementsByClassName(id, 'browser');
156 162 for (var i = 0; i < els.length; i++) {
157 163 if (els[i].hasClassName('open')) {
158 164 collapseScmEntry(els[i].id);
159 165 }
160 166 Element.hide(els[i]);
161 167 }
162 168 $(id).removeClassName('open');
163 169 }
164 170
165 171 function expandScmEntry(id) {
166 172 var els = document.getElementsByClassName(id, 'browser');
167 173 for (var i = 0; i < els.length; i++) {
168 174 Element.show(els[i]);
169 175 if (els[i].hasClassName('loaded') && !els[i].hasClassName('collapsed')) {
170 176 expandScmEntry(els[i].id);
171 177 }
172 178 }
173 179 $(id).addClassName('open');
174 180 }
175 181
176 182 function scmEntryClick(id) {
177 183 el = $(id);
178 184 if (el.hasClassName('open')) {
179 185 collapseScmEntry(id);
180 186 el.addClassName('collapsed');
181 187 return false;
182 188 } else if (el.hasClassName('loaded')) {
183 189 expandScmEntry(id);
184 190 el.removeClassName('collapsed');
185 191 return false;
186 192 }
187 193 if (el.hasClassName('loading')) {
188 194 return false;
189 195 }
190 196 el.addClassName('loading');
191 197 return true;
192 198 }
193 199
194 200 function scmEntryLoaded(id) {
195 201 Element.addClassName(id, 'open');
196 202 Element.addClassName(id, 'loaded');
197 203 Element.removeClassName(id, 'loading');
198 204 }
199 205
200 206 function randomKey(size) {
201 207 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
202 208 var key = '';
203 209 for (i = 0; i < size; i++) {
204 210 key += chars[Math.floor(Math.random() * chars.length)];
205 211 }
206 212 return key;
207 213 }
208 214
209 215 function observeParentIssueField(url) {
210 216 new Ajax.Autocompleter('issue_parent_issue_id',
211 217 'parent_issue_candidates',
212 218 url,
213 219 { minChars: 3,
214 220 frequency: 0.5,
215 221 paramName: 'q',
216 222 updateElement: function(value) {
217 223 document.getElementById('issue_parent_issue_id').value = value.id;
218 224 }});
219 225 }
220 226
221 227 function observeRelatedIssueField(url) {
222 228 new Ajax.Autocompleter('relation_issue_to_id',
223 229 'related_issue_candidates',
224 230 url,
225 231 { minChars: 3,
226 232 frequency: 0.5,
227 233 paramName: 'q',
228 234 updateElement: function(value) {
229 235 document.getElementById('relation_issue_to_id').value = value.id;
230 236 },
231 237 parameters: 'scope=all'
232 238 });
233 239 }
234 240
235 241 function setVisible(id, visible) {
236 242 var el = $(id);
237 243 if (el) {if (visible) {el.show();} else {el.hide();}}
238 244 }
239 245
240 246 function observeProjectModules() {
241 247 var f = function() {
242 248 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
243 249 var c = ($('project_enabled_module_names_issue_tracking').checked == true);
244 250 setVisible('project_trackers', c);
245 251 setVisible('project_issue_custom_fields', c);
246 252 };
247 253
248 254 Event.observe(window, 'load', f);
249 255 Event.observe('project_enabled_module_names_issue_tracking', 'change', f);
250 256 }
251 257
252 258
253 259 /* shows and hides ajax indicator */
254 260 Ajax.Responders.register({
255 261 onCreate: function(){
256 262 if ($('ajax-indicator') && Ajax.activeRequestCount > 0) {
257 263 Element.show('ajax-indicator');
258 264 }
259 265 },
260 266 onComplete: function(){
261 267 if ($('ajax-indicator') && Ajax.activeRequestCount == 0) {
262 268 Element.hide('ajax-indicator');
263 269 }
264 270 }
265 271 });
266 272
267 273 function hideOnLoad() {
268 274 $$('.hol').each(function(el) {
269 275 el.hide();
270 276 });
271 277 }
272 278
273 279 Event.observe(window, 'load', hideOnLoad);
@@ -1,31 +1,37
1 1 ---
2 issue_statuses_006:
3 name: Rejected
4 is_default: false
5 is_closed: true
6 id: 6
7 2 issue_statuses_001:
3 id: 1
8 4 name: New
9 5 is_default: true
10 6 is_closed: false
11 id: 1
7 position: 1
12 8 issue_statuses_002:
9 id: 2
13 10 name: Assigned
14 11 is_default: false
15 12 is_closed: false
16 id: 2
13 position: 2
17 14 issue_statuses_003:
15 id: 3
18 16 name: Resolved
19 17 is_default: false
20 18 is_closed: false
21 id: 3
19 position: 3
22 20 issue_statuses_004:
23 21 name: Feedback
22 id: 4
24 23 is_default: false
25 24 is_closed: false
26 id: 4
25 position: 4
27 26 issue_statuses_005:
27 id: 5
28 28 name: Closed
29 29 is_default: false
30 30 is_closed: true
31 id: 5
31 position: 5
32 issue_statuses_006:
33 id: 6
34 name: Rejected
35 is_default: false
36 is_closed: true
37 position: 6
@@ -1,158 +1,186
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.expand_path('../../test_helper', __FILE__)
19 19 require 'workflows_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class WorkflowsController; def rescue_action(e) raise e end; end
23 23
24 24 class WorkflowsControllerTest < ActionController::TestCase
25 25 fixtures :roles, :trackers, :workflows, :users, :issue_statuses
26 26
27 27 def setup
28 28 @controller = WorkflowsController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 @request.session[:user_id] = 1 # admin
33 33 end
34 34
35 35 def test_index
36 36 get :index
37 37 assert_response :success
38 38 assert_template 'index'
39 39
40 40 count = Workflow.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2')
41 41 assert_tag :tag => 'a', :content => count.to_s,
42 42 :attributes => { :href => '/workflows/edit?role_id=1&amp;tracker_id=2' }
43 43 end
44 44
45 45 def test_get_edit
46 46 get :edit
47 47 assert_response :success
48 48 assert_template 'edit'
49 49 assert_not_nil assigns(:roles)
50 50 assert_not_nil assigns(:trackers)
51 51 end
52 52
53 53 def test_get_edit_with_role_and_tracker
54 54 Workflow.delete_all
55 55 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
56 56 Workflow.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
57 57
58 58 get :edit, :role_id => 2, :tracker_id => 1
59 59 assert_response :success
60 60 assert_template 'edit'
61 61
62 62 # used status only
63 63 assert_not_nil assigns(:statuses)
64 64 assert_equal [2, 3, 5], assigns(:statuses).collect(&:id)
65 65
66 66 # allowed transitions
67 67 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
68 :name => 'issue_status[3][]',
69 :value => '5',
68 :name => 'issue_status[3][5][]',
69 :value => 'always',
70 70 :checked => 'checked' }
71 71 # not allowed
72 72 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
73 :name => 'issue_status[3][]',
74 :value => '2',
73 :name => 'issue_status[3][2][]',
74 :value => 'always',
75 75 :checked => nil }
76 76 # unused
77 77 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
78 :name => 'issue_status[4][]' }
78 :name => 'issue_status[1][1][]' }
79 79 end
80 80
81 81 def test_get_edit_with_role_and_tracker_and_all_statuses
82 82 Workflow.delete_all
83 83
84 84 get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0'
85 85 assert_response :success
86 86 assert_template 'edit'
87 87
88 88 assert_not_nil assigns(:statuses)
89 89 assert_equal IssueStatus.count, assigns(:statuses).size
90 90
91 91 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
92 :name => 'issue_status[1][]',
93 :value => '1',
92 :name => 'issue_status[1][1][]',
93 :value => 'always',
94 94 :checked => nil }
95 95 end
96 96
97 97 def test_post_edit
98 post :edit, :role_id => 2, :tracker_id => 1, :issue_status => {'4' => ['5'], '3' => ['1', '2']}
98 post :edit, :role_id => 2, :tracker_id => 1,
99 :issue_status => {
100 '4' => {'5' => ['always']},
101 '3' => {'1' => ['always'], '2' => ['always']}
102 }
99 103 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
100 104
101 105 assert_equal 3, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
102 106 assert_not_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
103 107 assert_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4})
104 108 end
105 109
110 def test_post_edit_with_additional_transitions
111 post :edit, :role_id => 2, :tracker_id => 1,
112 :issue_status => {
113 '4' => {'5' => ['always']},
114 '3' => {'1' => ['author'], '2' => ['assignee'], '4' => ['author', 'assignee']}
115 }
116 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
117
118 assert_equal 4, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
119
120 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5})
121 assert ! w.author
122 assert ! w.assignee
123 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1})
124 assert w.author
125 assert ! w.assignee
126 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
127 assert ! w.author
128 assert w.assignee
129 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4})
130 assert w.author
131 assert w.assignee
132 end
133
106 134 def test_clear_workflow
107 135 assert Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0
108 136
109 137 post :edit, :role_id => 2, :tracker_id => 1
110 138 assert_equal 0, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
111 139 end
112 140
113 141 def test_get_copy
114 142 get :copy
115 143 assert_response :success
116 144 assert_template 'copy'
117 145 end
118 146
119 147 def test_post_copy_one_to_one
120 148 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
121 149
122 150 post :copy, :source_tracker_id => '1', :source_role_id => '2',
123 151 :target_tracker_ids => ['3'], :target_role_ids => ['1']
124 152 assert_response 302
125 153 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
126 154 end
127 155
128 156 def test_post_copy_one_to_many
129 157 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
130 158
131 159 post :copy, :source_tracker_id => '1', :source_role_id => '2',
132 160 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
133 161 assert_response 302
134 162 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1)
135 163 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
136 164 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3)
137 165 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3)
138 166 end
139 167
140 168 def test_post_copy_many_to_many
141 169 source_t2 = status_transitions(:tracker_id => 2, :role_id => 2)
142 170 source_t3 = status_transitions(:tracker_id => 3, :role_id => 2)
143 171
144 172 post :copy, :source_tracker_id => 'any', :source_role_id => '2',
145 173 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
146 174 assert_response 302
147 175 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1)
148 176 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1)
149 177 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3)
150 178 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3)
151 179 end
152 180
153 181 # Returns an array of status transitions that can be compared
154 182 def status_transitions(conditions)
155 183 Workflow.find(:all, :conditions => conditions,
156 184 :order => 'tracker_id, role_id, old_status_id, new_status_id').collect {|w| [w.old_status, w.new_status_id]}
157 185 end
158 186 end
@@ -1,107 +1,131
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssueStatusTest < ActiveSupport::TestCase
21 fixtures :issue_statuses, :issues
21 fixtures :issue_statuses, :issues, :roles, :trackers
22 22
23 23 def test_create
24 24 status = IssueStatus.new :name => "Assigned"
25 25 assert !status.save
26 26 # status name uniqueness
27 27 assert_equal 1, status.errors.count
28 28
29 29 status.name = "Test Status"
30 30 assert status.save
31 31 assert !status.is_default
32 32 end
33 33
34 34 def test_destroy
35 35 status = IssueStatus.find(3)
36 36 assert_difference 'IssueStatus.count', -1 do
37 37 assert status.destroy
38 38 end
39 39 assert_nil Workflow.first(:conditions => {:old_status_id => status.id})
40 40 assert_nil Workflow.first(:conditions => {:new_status_id => status.id})
41 41 end
42 42
43 43 def test_destroy_status_in_use
44 44 # Status assigned to an Issue
45 45 status = Issue.find(1).status
46 46 assert_raise(RuntimeError, "Can't delete status") { status.destroy }
47 47 end
48 48
49 49 def test_default
50 50 status = IssueStatus.default
51 51 assert_kind_of IssueStatus, status
52 52 end
53 53
54 54 def test_change_default
55 55 status = IssueStatus.find(2)
56 56 assert !status.is_default
57 57 status.is_default = true
58 58 assert status.save
59 59 status.reload
60 60
61 61 assert_equal status, IssueStatus.default
62 62 assert !IssueStatus.find(1).is_default
63 63 end
64 64
65 65 def test_reorder_should_not_clear_default_status
66 66 status = IssueStatus.default
67 67 status.move_to_bottom
68 68 status.reload
69 69 assert status.is_default?
70 70 end
71 71
72 def test_new_statuses_allowed_to
73 Workflow.delete_all
74
75 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
76 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
77 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
78 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
79 status = IssueStatus.find(1)
80 role = Role.find(1)
81 tracker = Tracker.find(1)
82
83 assert_equal [2], status.new_statuses_allowed_to([role], tracker, false, false).map(&:id)
84 assert_equal [2], status.find_new_statuses_allowed_to([role], tracker, false, false).map(&:id)
85
86 assert_equal [2, 3], status.new_statuses_allowed_to([role], tracker, true, false).map(&:id)
87 assert_equal [2, 3], status.find_new_statuses_allowed_to([role], tracker, true, false).map(&:id)
88
89 assert_equal [2, 4], status.new_statuses_allowed_to([role], tracker, false, true).map(&:id)
90 assert_equal [2, 4], status.find_new_statuses_allowed_to([role], tracker, false, true).map(&:id)
91
92 assert_equal [2, 3, 4, 5], status.new_statuses_allowed_to([role], tracker, true, true).map(&:id)
93 assert_equal [2, 3, 4, 5], status.find_new_statuses_allowed_to([role], tracker, true, true).map(&:id)
94 end
95
72 96 context "#update_done_ratios" do
73 97 setup do
74 98 @issue = Issue.find(1)
75 99 @issue_status = IssueStatus.find(1)
76 100 @issue_status.update_attribute(:default_done_ratio, 50)
77 101 end
78 102
79 103 context "with Setting.issue_done_ratio using the issue_field" do
80 104 setup do
81 105 Setting.issue_done_ratio = 'issue_field'
82 106 end
83 107
84 108 should "change nothing" do
85 109 IssueStatus.update_issue_done_ratios
86 110
87 111 assert_equal 0, Issue.count(:conditions => {:done_ratio => 50})
88 112 end
89 113 end
90 114
91 115 context "with Setting.issue_done_ratio using the issue_status" do
92 116 setup do
93 117 Setting.issue_done_ratio = 'issue_status'
94 118 end
95 119
96 120 should "update all of the issue's done_ratios to match their Issue Status" do
97 121 IssueStatus.update_issue_done_ratios
98 122
99 123 issues = Issue.find([1,3,4,5,6,7,9,10])
100 124 issues.each do |issue|
101 125 assert_equal @issue_status, issue.status
102 126 assert_equal 50, issue.read_attribute(:done_ratio)
103 127 end
104 128 end
105 129 end
106 130 end
107 131 end
@@ -1,835 +1,862
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssueTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles,
22 22 :trackers, :projects_trackers,
23 23 :enabled_modules,
24 24 :versions,
25 25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 26 :enumerations,
27 27 :issues,
28 28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 29 :time_entries
30 30
31 31 def test_create
32 32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
33 33 assert issue.save
34 34 issue.reload
35 35 assert_equal 1.5, issue.estimated_hours
36 36 end
37 37
38 38 def test_create_minimal
39 39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
40 40 assert issue.save
41 41 assert issue.description.nil?
42 42 end
43 43
44 44 def test_create_with_required_custom_field
45 45 field = IssueCustomField.find_by_name('Database')
46 46 field.update_attribute(:is_required, true)
47 47
48 48 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
49 49 assert issue.available_custom_fields.include?(field)
50 50 # No value for the custom field
51 51 assert !issue.save
52 52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
53 53 # Blank value
54 54 issue.custom_field_values = { field.id => '' }
55 55 assert !issue.save
56 56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
57 57 # Invalid value
58 58 issue.custom_field_values = { field.id => 'SQLServer' }
59 59 assert !issue.save
60 60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
61 61 # Valid value
62 62 issue.custom_field_values = { field.id => 'PostgreSQL' }
63 63 assert issue.save
64 64 issue.reload
65 65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
66 66 end
67 67
68 68 def test_visible_scope_for_anonymous
69 69 # Anonymous user should see issues of public projects only
70 70 issues = Issue.visible(User.anonymous).all
71 71 assert issues.any?
72 72 assert_nil issues.detect {|issue| !issue.project.is_public?}
73 73 # Anonymous user should not see issues without permission
74 74 Role.anonymous.remove_permission!(:view_issues)
75 75 issues = Issue.visible(User.anonymous).all
76 76 assert issues.empty?
77 77 end
78 78
79 79 def test_visible_scope_for_user
80 80 user = User.find(9)
81 81 assert user.projects.empty?
82 82 # Non member user should see issues of public projects only
83 83 issues = Issue.visible(user).all
84 84 assert issues.any?
85 85 assert_nil issues.detect {|issue| !issue.project.is_public?}
86 86 # Non member user should not see issues without permission
87 87 Role.non_member.remove_permission!(:view_issues)
88 88 user.reload
89 89 issues = Issue.visible(user).all
90 90 assert issues.empty?
91 91 # User should see issues of projects for which he has view_issues permissions only
92 92 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
93 93 user.reload
94 94 issues = Issue.visible(user).all
95 95 assert issues.any?
96 96 assert_nil issues.detect {|issue| issue.project_id != 2}
97 97 end
98 98
99 99 def test_visible_scope_for_admin
100 100 user = User.find(1)
101 101 user.members.each(&:destroy)
102 102 assert user.projects.empty?
103 103 issues = Issue.visible(user).all
104 104 assert issues.any?
105 105 # Admin should see issues on private projects that he does not belong to
106 106 assert issues.detect {|issue| !issue.project.is_public?}
107 107 end
108 108
109 109 def test_errors_full_messages_should_include_custom_fields_errors
110 110 field = IssueCustomField.find_by_name('Database')
111 111
112 112 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
113 113 assert issue.available_custom_fields.include?(field)
114 114 # Invalid value
115 115 issue.custom_field_values = { field.id => 'SQLServer' }
116 116
117 117 assert !issue.valid?
118 118 assert_equal 1, issue.errors.full_messages.size
119 119 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
120 120 end
121 121
122 122 def test_update_issue_with_required_custom_field
123 123 field = IssueCustomField.find_by_name('Database')
124 124 field.update_attribute(:is_required, true)
125 125
126 126 issue = Issue.find(1)
127 127 assert_nil issue.custom_value_for(field)
128 128 assert issue.available_custom_fields.include?(field)
129 129 # No change to custom values, issue can be saved
130 130 assert issue.save
131 131 # Blank value
132 132 issue.custom_field_values = { field.id => '' }
133 133 assert !issue.save
134 134 # Valid value
135 135 issue.custom_field_values = { field.id => 'PostgreSQL' }
136 136 assert issue.save
137 137 issue.reload
138 138 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
139 139 end
140 140
141 141 def test_should_not_update_attributes_if_custom_fields_validation_fails
142 142 issue = Issue.find(1)
143 143 field = IssueCustomField.find_by_name('Database')
144 144 assert issue.available_custom_fields.include?(field)
145 145
146 146 issue.custom_field_values = { field.id => 'Invalid' }
147 147 issue.subject = 'Should be not be saved'
148 148 assert !issue.save
149 149
150 150 issue.reload
151 151 assert_equal "Can't print recipes", issue.subject
152 152 end
153 153
154 154 def test_should_not_recreate_custom_values_objects_on_update
155 155 field = IssueCustomField.find_by_name('Database')
156 156
157 157 issue = Issue.find(1)
158 158 issue.custom_field_values = { field.id => 'PostgreSQL' }
159 159 assert issue.save
160 160 custom_value = issue.custom_value_for(field)
161 161 issue.reload
162 162 issue.custom_field_values = { field.id => 'MySQL' }
163 163 assert issue.save
164 164 issue.reload
165 165 assert_equal custom_value.id, issue.custom_value_for(field).id
166 166 end
167 167
168 168 def test_assigning_tracker_id_should_reload_custom_fields_values
169 169 issue = Issue.new(:project => Project.find(1))
170 170 assert issue.custom_field_values.empty?
171 171 issue.tracker_id = 1
172 172 assert issue.custom_field_values.any?
173 173 end
174 174
175 175 def test_assigning_attributes_should_assign_tracker_id_first
176 176 attributes = ActiveSupport::OrderedHash.new
177 177 attributes['custom_field_values'] = { '1' => 'MySQL' }
178 178 attributes['tracker_id'] = '1'
179 179 issue = Issue.new(:project => Project.find(1))
180 180 issue.attributes = attributes
181 181 assert_not_nil issue.custom_value_for(1)
182 182 assert_equal 'MySQL', issue.custom_value_for(1).value
183 183 end
184 184
185 185 def test_should_update_issue_with_disabled_tracker
186 186 p = Project.find(1)
187 187 issue = Issue.find(1)
188 188
189 189 p.trackers.delete(issue.tracker)
190 190 assert !p.trackers.include?(issue.tracker)
191 191
192 192 issue.reload
193 193 issue.subject = 'New subject'
194 194 assert issue.save
195 195 end
196 196
197 197 def test_should_not_set_a_disabled_tracker
198 198 p = Project.find(1)
199 199 p.trackers.delete(Tracker.find(2))
200 200
201 201 issue = Issue.find(1)
202 202 issue.tracker_id = 2
203 203 issue.subject = 'New subject'
204 204 assert !issue.save
205 205 assert_not_nil issue.errors.on(:tracker_id)
206 206 end
207 207
208 208 def test_category_based_assignment
209 209 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
210 210 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
211 211 end
212 212
213
214
215 def test_new_statuses_allowed_to
216 Workflow.delete_all
217
218 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
219 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
220 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
221 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
222 status = IssueStatus.find(1)
223 role = Role.find(1)
224 tracker = Tracker.find(1)
225 user = User.find(2)
226
227 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
228 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
229
230 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
231 assert_equal [1, 2, 3], issue.new_statuses_allowed_to(user).map(&:id)
232
233 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
234 assert_equal [1, 2, 4], issue.new_statuses_allowed_to(user).map(&:id)
235
236 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
237 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
238 end
239
213 240 def test_copy
214 241 issue = Issue.new.copy_from(1)
215 242 assert issue.save
216 243 issue.reload
217 244 orig = Issue.find(1)
218 245 assert_equal orig.subject, issue.subject
219 246 assert_equal orig.tracker, issue.tracker
220 247 assert_equal "125", issue.custom_value_for(2).value
221 248 end
222 249
223 250 def test_copy_should_copy_status
224 251 orig = Issue.find(8)
225 252 assert orig.status != IssueStatus.default
226 253
227 254 issue = Issue.new.copy_from(orig)
228 255 assert issue.save
229 256 issue.reload
230 257 assert_equal orig.status, issue.status
231 258 end
232 259
233 260 def test_should_close_duplicates
234 261 # Create 3 issues
235 262 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
236 263 assert issue1.save
237 264 issue2 = issue1.clone
238 265 assert issue2.save
239 266 issue3 = issue1.clone
240 267 assert issue3.save
241 268
242 269 # 2 is a dupe of 1
243 270 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
244 271 # And 3 is a dupe of 2
245 272 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
246 273 # And 3 is a dupe of 1 (circular duplicates)
247 274 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
248 275
249 276 assert issue1.reload.duplicates.include?(issue2)
250 277
251 278 # Closing issue 1
252 279 issue1.init_journal(User.find(:first), "Closing issue1")
253 280 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
254 281 assert issue1.save
255 282 # 2 and 3 should be also closed
256 283 assert issue2.reload.closed?
257 284 assert issue3.reload.closed?
258 285 end
259 286
260 287 def test_should_not_close_duplicated_issue
261 288 # Create 3 issues
262 289 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
263 290 assert issue1.save
264 291 issue2 = issue1.clone
265 292 assert issue2.save
266 293
267 294 # 2 is a dupe of 1
268 295 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
269 296 # 2 is a dup of 1 but 1 is not a duplicate of 2
270 297 assert !issue2.reload.duplicates.include?(issue1)
271 298
272 299 # Closing issue 2
273 300 issue2.init_journal(User.find(:first), "Closing issue2")
274 301 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
275 302 assert issue2.save
276 303 # 1 should not be also closed
277 304 assert !issue1.reload.closed?
278 305 end
279 306
280 307 def test_assignable_versions
281 308 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
282 309 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
283 310 end
284 311
285 312 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
286 313 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
287 314 assert !issue.save
288 315 assert_not_nil issue.errors.on(:fixed_version_id)
289 316 end
290 317
291 318 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
292 319 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
293 320 assert !issue.save
294 321 assert_not_nil issue.errors.on(:fixed_version_id)
295 322 end
296 323
297 324 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
298 325 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
299 326 assert issue.save
300 327 end
301 328
302 329 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
303 330 issue = Issue.find(11)
304 331 assert_equal 'closed', issue.fixed_version.status
305 332 issue.subject = 'Subject changed'
306 333 assert issue.save
307 334 end
308 335
309 336 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
310 337 issue = Issue.find(11)
311 338 issue.status_id = 1
312 339 assert !issue.save
313 340 assert_not_nil issue.errors.on_base
314 341 end
315 342
316 343 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
317 344 issue = Issue.find(11)
318 345 issue.status_id = 1
319 346 issue.fixed_version_id = 3
320 347 assert issue.save
321 348 end
322 349
323 350 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
324 351 issue = Issue.find(12)
325 352 assert_equal 'locked', issue.fixed_version.status
326 353 issue.status_id = 1
327 354 assert issue.save
328 355 end
329 356
330 357 def test_move_to_another_project_with_same_category
331 358 issue = Issue.find(1)
332 359 assert issue.move_to_project(Project.find(2))
333 360 issue.reload
334 361 assert_equal 2, issue.project_id
335 362 # Category changes
336 363 assert_equal 4, issue.category_id
337 364 # Make sure time entries were move to the target project
338 365 assert_equal 2, issue.time_entries.first.project_id
339 366 end
340 367
341 368 def test_move_to_another_project_without_same_category
342 369 issue = Issue.find(2)
343 370 assert issue.move_to_project(Project.find(2))
344 371 issue.reload
345 372 assert_equal 2, issue.project_id
346 373 # Category cleared
347 374 assert_nil issue.category_id
348 375 end
349 376
350 377 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
351 378 issue = Issue.find(1)
352 379 issue.update_attribute(:fixed_version_id, 1)
353 380 assert issue.move_to_project(Project.find(2))
354 381 issue.reload
355 382 assert_equal 2, issue.project_id
356 383 # Cleared fixed_version
357 384 assert_equal nil, issue.fixed_version
358 385 end
359 386
360 387 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
361 388 issue = Issue.find(1)
362 389 issue.update_attribute(:fixed_version_id, 4)
363 390 assert issue.move_to_project(Project.find(5))
364 391 issue.reload
365 392 assert_equal 5, issue.project_id
366 393 # Keep fixed_version
367 394 assert_equal 4, issue.fixed_version_id
368 395 end
369 396
370 397 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
371 398 issue = Issue.find(1)
372 399 issue.update_attribute(:fixed_version_id, 1)
373 400 assert issue.move_to_project(Project.find(5))
374 401 issue.reload
375 402 assert_equal 5, issue.project_id
376 403 # Cleared fixed_version
377 404 assert_equal nil, issue.fixed_version
378 405 end
379 406
380 407 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
381 408 issue = Issue.find(1)
382 409 issue.update_attribute(:fixed_version_id, 7)
383 410 assert issue.move_to_project(Project.find(2))
384 411 issue.reload
385 412 assert_equal 2, issue.project_id
386 413 # Keep fixed_version
387 414 assert_equal 7, issue.fixed_version_id
388 415 end
389 416
390 417 def test_move_to_another_project_with_disabled_tracker
391 418 issue = Issue.find(1)
392 419 target = Project.find(2)
393 420 target.tracker_ids = [3]
394 421 target.save
395 422 assert_equal false, issue.move_to_project(target)
396 423 issue.reload
397 424 assert_equal 1, issue.project_id
398 425 end
399 426
400 427 def test_copy_to_the_same_project
401 428 issue = Issue.find(1)
402 429 copy = nil
403 430 assert_difference 'Issue.count' do
404 431 copy = issue.move_to_project(issue.project, nil, :copy => true)
405 432 end
406 433 assert_kind_of Issue, copy
407 434 assert_equal issue.project, copy.project
408 435 assert_equal "125", copy.custom_value_for(2).value
409 436 end
410 437
411 438 def test_copy_to_another_project_and_tracker
412 439 issue = Issue.find(1)
413 440 copy = nil
414 441 assert_difference 'Issue.count' do
415 442 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
416 443 end
417 444 copy.reload
418 445 assert_kind_of Issue, copy
419 446 assert_equal Project.find(3), copy.project
420 447 assert_equal Tracker.find(2), copy.tracker
421 448 # Custom field #2 is not associated with target tracker
422 449 assert_nil copy.custom_value_for(2)
423 450 end
424 451
425 452 context "#move_to_project" do
426 453 context "as a copy" do
427 454 setup do
428 455 @issue = Issue.find(1)
429 456 @copy = nil
430 457 end
431 458
432 459 should "allow assigned_to changes" do
433 460 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
434 461 assert_equal 3, @copy.assigned_to_id
435 462 end
436 463
437 464 should "allow status changes" do
438 465 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
439 466 assert_equal 2, @copy.status_id
440 467 end
441 468
442 469 should "allow start date changes" do
443 470 date = Date.today
444 471 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
445 472 assert_equal date, @copy.start_date
446 473 end
447 474
448 475 should "allow due date changes" do
449 476 date = Date.today
450 477 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
451 478
452 479 assert_equal date, @copy.due_date
453 480 end
454 481 end
455 482 end
456 483
457 484 def test_recipients_should_not_include_users_that_cannot_view_the_issue
458 485 issue = Issue.find(12)
459 486 assert issue.recipients.include?(issue.author.mail)
460 487 # move the issue to a private project
461 488 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
462 489 # author is not a member of project anymore
463 490 assert !copy.recipients.include?(copy.author.mail)
464 491 end
465 492
466 493 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
467 494 user = User.find(3)
468 495 issue = Issue.find(9)
469 496 Watcher.create!(:user => user, :watchable => issue)
470 497 assert issue.watched_by?(user)
471 498 assert !issue.watcher_recipients.include?(user.mail)
472 499 end
473 500
474 501 def test_issue_destroy
475 502 Issue.find(1).destroy
476 503 assert_nil Issue.find_by_id(1)
477 504 assert_nil TimeEntry.find_by_issue_id(1)
478 505 end
479 506
480 507 def test_blocked
481 508 blocked_issue = Issue.find(9)
482 509 blocking_issue = Issue.find(10)
483 510
484 511 assert blocked_issue.blocked?
485 512 assert !blocking_issue.blocked?
486 513 end
487 514
488 515 def test_blocked_issues_dont_allow_closed_statuses
489 516 blocked_issue = Issue.find(9)
490 517
491 518 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
492 519 assert !allowed_statuses.empty?
493 520 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
494 521 assert closed_statuses.empty?
495 522 end
496 523
497 524 def test_unblocked_issues_allow_closed_statuses
498 525 blocking_issue = Issue.find(10)
499 526
500 527 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
501 528 assert !allowed_statuses.empty?
502 529 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
503 530 assert !closed_statuses.empty?
504 531 end
505 532
506 533 def test_rescheduling_an_issue_should_reschedule_following_issue
507 534 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
508 535 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
509 536 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
510 537 assert_equal issue1.due_date + 1, issue2.reload.start_date
511 538
512 539 issue1.due_date = Date.today + 5
513 540 issue1.save!
514 541 assert_equal issue1.due_date + 1, issue2.reload.start_date
515 542 end
516 543
517 544 def test_overdue
518 545 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
519 546 assert !Issue.new(:due_date => Date.today).overdue?
520 547 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
521 548 assert !Issue.new(:due_date => nil).overdue?
522 549 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
523 550 end
524 551
525 552 context "#behind_schedule?" do
526 553 should "be false if the issue has no start_date" do
527 554 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
528 555 end
529 556
530 557 should "be false if the issue has no end_date" do
531 558 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
532 559 end
533 560
534 561 should "be false if the issue has more done than it's calendar time" do
535 562 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
536 563 end
537 564
538 565 should "be true if the issue hasn't been started at all" do
539 566 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
540 567 end
541 568
542 569 should "be true if the issue has used more calendar time than it's done ratio" do
543 570 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
544 571 end
545 572 end
546 573
547 574 context "#assignable_users" do
548 575 should "be Users" do
549 576 assert_kind_of User, Issue.find(1).assignable_users.first
550 577 end
551 578
552 579 should "include the issue author" do
553 580 project = Project.find(1)
554 581 non_project_member = User.generate!
555 582 issue = Issue.generate_for_project!(project, :author => non_project_member)
556 583
557 584 assert issue.assignable_users.include?(non_project_member)
558 585 end
559 586
560 587 should "not show the issue author twice" do
561 588 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
562 589 assert_equal 2, assignable_user_ids.length
563 590
564 591 assignable_user_ids.each do |user_id|
565 592 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
566 593 end
567 594 end
568 595 end
569 596
570 597 def test_create_should_send_email_notification
571 598 ActionMailer::Base.deliveries.clear
572 599 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
573 600
574 601 assert issue.save
575 602 assert_equal 1, ActionMailer::Base.deliveries.size
576 603 end
577 604
578 605 def test_stale_issue_should_not_send_email_notification
579 606 ActionMailer::Base.deliveries.clear
580 607 issue = Issue.find(1)
581 608 stale = Issue.find(1)
582 609
583 610 issue.init_journal(User.find(1))
584 611 issue.subject = 'Subjet update'
585 612 assert issue.save
586 613 assert_equal 1, ActionMailer::Base.deliveries.size
587 614 ActionMailer::Base.deliveries.clear
588 615
589 616 stale.init_journal(User.find(1))
590 617 stale.subject = 'Another subjet update'
591 618 assert_raise ActiveRecord::StaleObjectError do
592 619 stale.save
593 620 end
594 621 assert ActionMailer::Base.deliveries.empty?
595 622 end
596 623
597 624 def test_saving_twice_should_not_duplicate_journal_details
598 625 i = Issue.find(:first)
599 626 i.init_journal(User.find(2), 'Some notes')
600 627 # initial changes
601 628 i.subject = 'New subject'
602 629 i.done_ratio = i.done_ratio + 10
603 630 assert_difference 'Journal.count' do
604 631 assert i.save
605 632 end
606 633 # 1 more change
607 634 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
608 635 assert_no_difference 'Journal.count' do
609 636 assert_difference 'JournalDetail.count', 1 do
610 637 i.save
611 638 end
612 639 end
613 640 # no more change
614 641 assert_no_difference 'Journal.count' do
615 642 assert_no_difference 'JournalDetail.count' do
616 643 i.save
617 644 end
618 645 end
619 646 end
620 647
621 648 def test_all_dependent_issues
622 649 IssueRelation.delete_all
623 650 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
624 651 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
625 652 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_PRECEDES)
626 653
627 654 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
628 655 end
629 656
630 657 def test_all_dependent_issues_with_persistent_circular_dependency
631 658 IssueRelation.delete_all
632 659 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
633 660 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
634 661 # Validation skipping
635 662 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
636 663
637 664 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
638 665 end
639 666
640 667 context "#done_ratio" do
641 668 setup do
642 669 @issue = Issue.find(1)
643 670 @issue_status = IssueStatus.find(1)
644 671 @issue_status.update_attribute(:default_done_ratio, 50)
645 672 @issue2 = Issue.find(2)
646 673 @issue_status2 = IssueStatus.find(2)
647 674 @issue_status2.update_attribute(:default_done_ratio, 0)
648 675 end
649 676
650 677 context "with Setting.issue_done_ratio using the issue_field" do
651 678 setup do
652 679 Setting.issue_done_ratio = 'issue_field'
653 680 end
654 681
655 682 should "read the issue's field" do
656 683 assert_equal 0, @issue.done_ratio
657 684 assert_equal 30, @issue2.done_ratio
658 685 end
659 686 end
660 687
661 688 context "with Setting.issue_done_ratio using the issue_status" do
662 689 setup do
663 690 Setting.issue_done_ratio = 'issue_status'
664 691 end
665 692
666 693 should "read the Issue Status's default done ratio" do
667 694 assert_equal 50, @issue.done_ratio
668 695 assert_equal 0, @issue2.done_ratio
669 696 end
670 697 end
671 698 end
672 699
673 700 context "#update_done_ratio_from_issue_status" do
674 701 setup do
675 702 @issue = Issue.find(1)
676 703 @issue_status = IssueStatus.find(1)
677 704 @issue_status.update_attribute(:default_done_ratio, 50)
678 705 @issue2 = Issue.find(2)
679 706 @issue_status2 = IssueStatus.find(2)
680 707 @issue_status2.update_attribute(:default_done_ratio, 0)
681 708 end
682 709
683 710 context "with Setting.issue_done_ratio using the issue_field" do
684 711 setup do
685 712 Setting.issue_done_ratio = 'issue_field'
686 713 end
687 714
688 715 should "not change the issue" do
689 716 @issue.update_done_ratio_from_issue_status
690 717 @issue2.update_done_ratio_from_issue_status
691 718
692 719 assert_equal 0, @issue.read_attribute(:done_ratio)
693 720 assert_equal 30, @issue2.read_attribute(:done_ratio)
694 721 end
695 722 end
696 723
697 724 context "with Setting.issue_done_ratio using the issue_status" do
698 725 setup do
699 726 Setting.issue_done_ratio = 'issue_status'
700 727 end
701 728
702 729 should "change the issue's done ratio" do
703 730 @issue.update_done_ratio_from_issue_status
704 731 @issue2.update_done_ratio_from_issue_status
705 732
706 733 assert_equal 50, @issue.read_attribute(:done_ratio)
707 734 assert_equal 0, @issue2.read_attribute(:done_ratio)
708 735 end
709 736 end
710 737 end
711 738
712 739 test "#by_tracker" do
713 740 groups = Issue.by_tracker(Project.find(1))
714 741 assert_equal 3, groups.size
715 742 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
716 743 end
717 744
718 745 test "#by_version" do
719 746 groups = Issue.by_version(Project.find(1))
720 747 assert_equal 3, groups.size
721 748 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
722 749 end
723 750
724 751 test "#by_priority" do
725 752 groups = Issue.by_priority(Project.find(1))
726 753 assert_equal 4, groups.size
727 754 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
728 755 end
729 756
730 757 test "#by_category" do
731 758 groups = Issue.by_category(Project.find(1))
732 759 assert_equal 2, groups.size
733 760 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
734 761 end
735 762
736 763 test "#by_assigned_to" do
737 764 groups = Issue.by_assigned_to(Project.find(1))
738 765 assert_equal 2, groups.size
739 766 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
740 767 end
741 768
742 769 test "#by_author" do
743 770 groups = Issue.by_author(Project.find(1))
744 771 assert_equal 4, groups.size
745 772 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
746 773 end
747 774
748 775 test "#by_subproject" do
749 776 groups = Issue.by_subproject(Project.find(1))
750 777 assert_equal 2, groups.size
751 778 assert_equal 5, groups.inject(0) {|sum, group| sum + group['total'].to_i}
752 779 end
753 780
754 781
755 782 context ".allowed_target_projects_on_move" do
756 783 should "return all active projects for admin users" do
757 784 User.current = User.find(1)
758 785 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
759 786 end
760 787
761 788 should "return allowed projects for non admin users" do
762 789 User.current = User.find(2)
763 790 Role.non_member.remove_permission! :move_issues
764 791 assert_equal 3, Issue.allowed_target_projects_on_move.size
765 792
766 793 Role.non_member.add_permission! :move_issues
767 794 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
768 795 end
769 796 end
770 797
771 798 def test_recently_updated_with_limit_scopes
772 799 #should return the last updated issue
773 800 assert_equal 1, Issue.recently_updated.with_limit(1).length
774 801 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
775 802 end
776 803
777 804 def test_on_active_projects_scope
778 805 assert Project.find(2).archive
779 806
780 807 before = Issue.on_active_project.length
781 808 # test inclusion to results
782 809 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
783 810 assert_equal before + 1, Issue.on_active_project.length
784 811
785 812 # Move to an archived project
786 813 issue.project = Project.find(2)
787 814 assert issue.save
788 815 assert_equal before, Issue.on_active_project.length
789 816 end
790 817
791 818 context "Issue#recipients" do
792 819 setup do
793 820 @project = Project.find(1)
794 821 @author = User.generate_with_protected!
795 822 @assignee = User.generate_with_protected!
796 823 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
797 824 end
798 825
799 826 should "include project recipients" do
800 827 assert @project.recipients.present?
801 828 @project.recipients.each do |project_recipient|
802 829 assert @issue.recipients.include?(project_recipient)
803 830 end
804 831 end
805 832
806 833 should "include the author if the author is active" do
807 834 assert @issue.author, "No author set for Issue"
808 835 assert @issue.recipients.include?(@issue.author.mail)
809 836 end
810 837
811 838 should "include the assigned to user if the assigned to user is active" do
812 839 assert @issue.assigned_to, "No assigned_to set for Issue"
813 840 assert @issue.recipients.include?(@issue.assigned_to.mail)
814 841 end
815 842
816 843 should "not include users who opt out of all email" do
817 844 @author.update_attribute(:mail_notification, :none)
818 845
819 846 assert !@issue.recipients.include?(@issue.author.mail)
820 847 end
821 848
822 849 should "not include the issue author if they are only notified of assigned issues" do
823 850 @author.update_attribute(:mail_notification, :only_assigned)
824 851
825 852 assert !@issue.recipients.include?(@issue.author.mail)
826 853 end
827 854
828 855 should "not include the assigned user if they are only notified of owned issues" do
829 856 @assignee.update_attribute(:mail_notification, :only_owner)
830 857
831 858 assert !@issue.recipients.include?(@issue.assigned_to.mail)
832 859 end
833 860
834 861 end
835 862 end
General Comments 0
You need to be logged in to leave comments. Login now