##// END OF EJS Templates
Makes new issue initial status settable in workflow (#5816)....
Jean-Philippe Lang -
r14076:2bc5b60f9de7
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,23
1 class InsertAllowedStatusesForNewIssues < ActiveRecord::Migration
2 def self.up
3 # Adds the default status for all trackers and roles
4 sql = "INSERT INTO #{WorkflowTransition.table_name} (tracker_id, old_status_id, new_status_id, role_id, type)" +
5 " SELECT t.id, 0, t.default_status_id, r.id, 'WorkflowTransition'" +
6 " FROM #{Tracker.table_name} t, #{Role.table_name} r"
7 WorkflowTransition.connection.execute(sql)
8
9 # Adds other statuses that are reachable with one transition
10 # to preserve previous behaviour as default
11 sql = "INSERT INTO #{WorkflowTransition.table_name} (tracker_id, old_status_id, new_status_id, role_id, type)" +
12 " SELECT t.id, 0, w.new_status_id, w.role_id, 'WorkflowTransition'" +
13 " FROM #{Tracker.table_name} t" +
14 " JOIN #{IssueStatus.table_name} s on s.id = t.default_status_id" +
15 " JOIN #{WorkflowTransition.table_name} w on w.tracker_id = t.id and w.old_status_id = s.id and w.type = 'WorkflowTransition'" +
16 " WHERE w.new_status_id <> t.default_status_id"
17 WorkflowTransition.connection.execute(sql)
18 end
19
20 def self.down
21 WorkflowTransition.where(:old_status_id => 0).delete_all
22 end
23 end
@@ -1,517 +1,517
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => [:new, :create]
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :update]
23 23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
24 24 before_filter :authorize, :except => [:index, :new, :create]
25 25 before_filter :find_optional_project, :only => [:index, :new, :create]
26 26 before_filter :build_new_issue_from_params, :only => [:new, :create]
27 27 accept_rss_auth :index, :show
28 28 accept_api_auth :index, :show, :create, :update, :destroy
29 29
30 30 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
31 31
32 32 helper :journals
33 33 helper :projects
34 34 helper :custom_fields
35 35 helper :issue_relations
36 36 helper :watchers
37 37 helper :attachments
38 38 helper :queries
39 39 include QueriesHelper
40 40 helper :repositories
41 41 helper :sort
42 42 include SortHelper
43 43 helper :timelog
44 44
45 45 def index
46 46 retrieve_query
47 47 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
48 48 sort_update(@query.sortable_columns)
49 49 @query.sort_criteria = sort_criteria.to_a
50 50
51 51 if @query.valid?
52 52 case params[:format]
53 53 when 'csv', 'pdf'
54 54 @limit = Setting.issues_export_limit.to_i
55 55 if params[:columns] == 'all'
56 56 @query.column_names = @query.available_inline_columns.map(&:name)
57 57 end
58 58 when 'atom'
59 59 @limit = Setting.feeds_limit.to_i
60 60 when 'xml', 'json'
61 61 @offset, @limit = api_offset_and_limit
62 62 @query.column_names = %w(author)
63 63 else
64 64 @limit = per_page_option
65 65 end
66 66
67 67 @issue_count = @query.issue_count
68 68 @issue_pages = Paginator.new @issue_count, @limit, params['page']
69 69 @offset ||= @issue_pages.offset
70 70 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
71 71 :order => sort_clause,
72 72 :offset => @offset,
73 73 :limit => @limit)
74 74 @issue_count_by_group = @query.issue_count_by_group
75 75
76 76 respond_to do |format|
77 77 format.html { render :template => 'issues/index', :layout => !request.xhr? }
78 78 format.api {
79 79 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
80 80 }
81 81 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
82 82 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
83 83 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
84 84 end
85 85 else
86 86 respond_to do |format|
87 87 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
88 88 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
89 89 format.api { render_validation_errors(@query) }
90 90 end
91 91 end
92 92 rescue ActiveRecord::RecordNotFound
93 93 render_404
94 94 end
95 95
96 96 def show
97 97 @journals = @issue.journals.includes(:user, :details).
98 98 references(:user, :details).
99 99 reorder(:created_on, :id).to_a
100 100 @journals.each_with_index {|j,i| j.indice = i+1}
101 101 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
102 102 Journal.preload_journals_details_custom_fields(@journals)
103 103 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
104 104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
105 105
106 106 @changesets = @issue.changesets.visible.preload(:repository, :user).to_a
107 107 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
108 108
109 109 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
110 110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
111 111 @priorities = IssuePriority.active
112 112 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
113 113 @relation = IssueRelation.new
114 114
115 115 respond_to do |format|
116 116 format.html {
117 117 retrieve_previous_and_next_issue_ids
118 118 render :template => 'issues/show'
119 119 }
120 120 format.api
121 121 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
122 122 format.pdf {
123 123 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
124 124 }
125 125 end
126 126 end
127 127
128 128 def new
129 129 respond_to do |format|
130 130 format.html { render :action => 'new', :layout => !request.xhr? }
131 131 format.js
132 132 end
133 133 end
134 134
135 135 def create
136 136 unless User.current.allowed_to?(:add_issues, @issue.project, :global => true)
137 137 raise ::Unauthorized
138 138 end
139 139 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
140 140 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
141 141 if @issue.save
142 142 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
143 143 respond_to do |format|
144 144 format.html {
145 145 render_attachment_warning_if_needed(@issue)
146 146 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
147 147 redirect_after_create
148 148 }
149 149 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
150 150 end
151 151 return
152 152 else
153 153 respond_to do |format|
154 154 format.html {
155 155 if @issue.project.nil?
156 156 render_error :status => 422
157 157 else
158 158 render :action => 'new'
159 159 end
160 160 }
161 161 format.api { render_validation_errors(@issue) }
162 162 end
163 163 end
164 164 end
165 165
166 166 def edit
167 167 return unless update_issue_from_params
168 168
169 169 respond_to do |format|
170 170 format.html { }
171 171 format.js
172 172 end
173 173 end
174 174
175 175 def update
176 176 return unless update_issue_from_params
177 177 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
178 178 saved = false
179 179 begin
180 180 saved = save_issue_with_child_records
181 181 rescue ActiveRecord::StaleObjectError
182 182 @conflict = true
183 183 if params[:last_journal_id]
184 184 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
185 185 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
186 186 end
187 187 end
188 188
189 189 if saved
190 190 render_attachment_warning_if_needed(@issue)
191 191 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
192 192
193 193 respond_to do |format|
194 194 format.html { redirect_back_or_default issue_path(@issue) }
195 195 format.api { render_api_ok }
196 196 end
197 197 else
198 198 respond_to do |format|
199 199 format.html { render :action => 'edit' }
200 200 format.api { render_validation_errors(@issue) }
201 201 end
202 202 end
203 203 end
204 204
205 205 # Bulk edit/copy a set of issues
206 206 def bulk_edit
207 207 @issues.sort!
208 208 @copy = params[:copy].present?
209 209 @notes = params[:notes]
210 210
211 211 if @copy
212 212 unless User.current.allowed_to?(:copy_issues, @projects)
213 213 raise ::Unauthorized
214 214 end
215 215 end
216 216
217 217 @allowed_projects = Issue.allowed_target_projects
218 218 if params[:issue]
219 219 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
220 220 if @target_project
221 221 target_projects = [@target_project]
222 222 end
223 223 end
224 224 target_projects ||= @projects
225 225
226 226 if @copy
227 227 # Copied issues will get their default statuses
228 228 @available_statuses = []
229 229 else
230 230 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
231 231 end
232 232 @custom_fields = @issues.map{|i|i.editable_custom_fields}.reduce(:&)
233 233 @assignables = target_projects.map(&:assignable_users).reduce(:&)
234 234 @trackers = target_projects.map(&:trackers).reduce(:&)
235 235 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
236 236 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
237 237 if @copy
238 238 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
239 239 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
240 240 end
241 241
242 242 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
243 243
244 244 @issue_params = params[:issue] || {}
245 245 @issue_params[:custom_field_values] ||= {}
246 246 end
247 247
248 248 def bulk_update
249 249 @issues.sort!
250 250 @copy = params[:copy].present?
251 251
252 252 attributes = parse_params_for_bulk_issue_attributes(params)
253 253 copy_subtasks = (params[:copy_subtasks] == '1')
254 254 copy_attachments = (params[:copy_attachments] == '1')
255 255
256 256 if @copy
257 257 unless User.current.allowed_to?(:copy_issues, @projects)
258 258 raise ::Unauthorized
259 259 end
260 260 target_projects = @projects
261 261 if attributes['project_id'].present?
262 262 target_projects = Project.where(:id => attributes['project_id']).to_a
263 263 end
264 264 unless User.current.allowed_to?(:add_issues, target_projects)
265 265 raise ::Unauthorized
266 266 end
267 267 end
268 268
269 269 unsaved_issues = []
270 270 saved_issues = []
271 271
272 272 if @copy && copy_subtasks
273 273 # Descendant issues will be copied with the parent task
274 274 # Don't copy them twice
275 275 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
276 276 end
277 277
278 278 @issues.each do |orig_issue|
279 279 orig_issue.reload
280 280 if @copy
281 281 issue = orig_issue.copy({},
282 282 :attachments => copy_attachments,
283 283 :subtasks => copy_subtasks,
284 284 :link => link_copy?(params[:link_copy])
285 285 )
286 286 else
287 287 issue = orig_issue
288 288 end
289 289 journal = issue.init_journal(User.current, params[:notes])
290 290 issue.safe_attributes = attributes
291 291 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
292 292 if issue.save
293 293 saved_issues << issue
294 294 else
295 295 unsaved_issues << orig_issue
296 296 end
297 297 end
298 298
299 299 if unsaved_issues.empty?
300 300 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
301 301 if params[:follow]
302 302 if @issues.size == 1 && saved_issues.size == 1
303 303 redirect_to issue_path(saved_issues.first)
304 304 elsif saved_issues.map(&:project).uniq.size == 1
305 305 redirect_to project_issues_path(saved_issues.map(&:project).first)
306 306 end
307 307 else
308 308 redirect_back_or_default _project_issues_path(@project)
309 309 end
310 310 else
311 311 @saved_issues = @issues
312 312 @unsaved_issues = unsaved_issues
313 313 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
314 314 bulk_edit
315 315 render :action => 'bulk_edit'
316 316 end
317 317 end
318 318
319 319 def destroy
320 320 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
321 321 if @hours > 0
322 322 case params[:todo]
323 323 when 'destroy'
324 324 # nothing to do
325 325 when 'nullify'
326 326 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
327 327 when 'reassign'
328 328 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
329 329 if reassign_to.nil?
330 330 flash.now[:error] = l(:error_issue_not_found_in_project)
331 331 return
332 332 else
333 333 TimeEntry.where(['issue_id IN (?)', @issues]).
334 334 update_all("issue_id = #{reassign_to.id}")
335 335 end
336 336 else
337 337 # display the destroy form if it's a user request
338 338 return unless api_request?
339 339 end
340 340 end
341 341 @issues.each do |issue|
342 342 begin
343 343 issue.reload.destroy
344 344 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
345 345 # nothing to do, issue was already deleted (eg. by a parent)
346 346 end
347 347 end
348 348 respond_to do |format|
349 349 format.html { redirect_back_or_default _project_issues_path(@project) }
350 350 format.api { render_api_ok }
351 351 end
352 352 end
353 353
354 354 private
355 355
356 356 def retrieve_previous_and_next_issue_ids
357 357 retrieve_query_from_session
358 358 if @query
359 359 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
360 360 sort_update(@query.sortable_columns, 'issues_index_sort')
361 361 limit = 500
362 362 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
363 363 if (idx = issue_ids.index(@issue.id)) && idx < limit
364 364 if issue_ids.size < 500
365 365 @issue_position = idx + 1
366 366 @issue_count = issue_ids.size
367 367 end
368 368 @prev_issue_id = issue_ids[idx - 1] if idx > 0
369 369 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
370 370 end
371 371 end
372 372 end
373 373
374 374 # Used by #edit and #update to set some common instance variables
375 375 # from the params
376 376 def update_issue_from_params
377 377 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
378 378 if params[:time_entry]
379 379 @time_entry.attributes = params[:time_entry]
380 380 end
381 381
382 382 @issue.init_journal(User.current)
383 383
384 384 issue_attributes = params[:issue]
385 385 if issue_attributes && params[:conflict_resolution]
386 386 case params[:conflict_resolution]
387 387 when 'overwrite'
388 388 issue_attributes = issue_attributes.dup
389 389 issue_attributes.delete(:lock_version)
390 390 when 'add_notes'
391 391 issue_attributes = issue_attributes.slice(:notes)
392 392 when 'cancel'
393 393 redirect_to issue_path(@issue)
394 394 return false
395 395 end
396 396 end
397 397 @issue.safe_attributes = issue_attributes
398 398 @priorities = IssuePriority.active
399 399 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
400 400 true
401 401 end
402 402
403 403 # Used by #new and #create to build a new issue from the params
404 404 # The new issue will be copied from an existing one if copy_from parameter is given
405 405 def build_new_issue_from_params
406 406 @issue = Issue.new
407 407 if params[:copy_from]
408 408 begin
409 409 @issue.init_journal(User.current)
410 410 @copy_from = Issue.visible.find(params[:copy_from])
411 411 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
412 412 raise ::Unauthorized
413 413 end
414 414 @link_copy = link_copy?(params[:link_copy]) || request.get?
415 415 @copy_attachments = params[:copy_attachments].present? || request.get?
416 416 @copy_subtasks = params[:copy_subtasks].present? || request.get?
417 417 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
418 418 rescue ActiveRecord::RecordNotFound
419 419 render_404
420 420 return
421 421 end
422 422 end
423 423 @issue.project = @project
424 424 if request.get?
425 425 @issue.project ||= @issue.allowed_target_projects.first
426 426 end
427 427 @issue.author ||= User.current
428 428 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
429 429
430 if attrs = params[:issue].deep_dup
431 if action_name == 'new' && params[:was_default_status] == attrs[:status_id]
432 attrs.delete(:status_id)
433 end
434 @issue.safe_attributes = attrs
430 attrs = (params[:issue] || {}).deep_dup
431 if action_name == 'new' && params[:was_default_status] == attrs[:status_id]
432 attrs.delete(:status_id)
435 433 end
434 @issue.safe_attributes = attrs
435
436 436 if @issue.project
437 437 @issue.tracker ||= @issue.project.trackers.first
438 438 if @issue.tracker.nil?
439 439 render_error l(:error_no_tracker_in_project)
440 440 return false
441 441 end
442 442 if @issue.status.nil?
443 443 render_error l(:error_no_default_issue_status)
444 444 return false
445 445 end
446 446 end
447 447
448 448 @priorities = IssuePriority.active
449 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, @issue.new_record?)
449 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
450 450 end
451 451
452 452 def parse_params_for_bulk_issue_attributes(params)
453 453 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
454 454 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
455 455 if custom = attributes[:custom_field_values]
456 456 custom.reject! {|k,v| v.blank?}
457 457 custom.keys.each do |k|
458 458 if custom[k].is_a?(Array)
459 459 custom[k] << '' if custom[k].delete('__none__')
460 460 else
461 461 custom[k] = '' if custom[k] == '__none__'
462 462 end
463 463 end
464 464 end
465 465 attributes
466 466 end
467 467
468 468 # Saves @issue and a time_entry from the parameters
469 469 def save_issue_with_child_records
470 470 Issue.transaction do
471 471 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
472 472 time_entry = @time_entry || TimeEntry.new
473 473 time_entry.project = @issue.project
474 474 time_entry.issue = @issue
475 475 time_entry.user = User.current
476 476 time_entry.spent_on = User.current.today
477 477 time_entry.attributes = params[:time_entry]
478 478 @issue.time_entries << time_entry
479 479 end
480 480
481 481 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
482 482 if @issue.save
483 483 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
484 484 else
485 485 raise ActiveRecord::Rollback
486 486 end
487 487 end
488 488 end
489 489
490 490 # Returns true if the issue copy should be linked
491 491 # to the original issue
492 492 def link_copy?(param)
493 493 case Setting.link_copied_issue
494 494 when 'yes'
495 495 true
496 496 when 'no'
497 497 false
498 498 when 'ask'
499 499 param == '1'
500 500 end
501 501 end
502 502
503 503 # Redirects user after a successful issue creation
504 504 def redirect_after_create
505 505 if params[:continue]
506 506 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
507 507 if params[:project_id]
508 508 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
509 509 else
510 510 attrs.merge! :project_id => @issue.project_id
511 511 redirect_to new_issue_path(:issue => attrs)
512 512 end
513 513 else
514 514 redirect_to issue_path(@issue)
515 515 end
516 516 end
517 517 end
@@ -1,142 +1,144
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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
23 23 def index
24 24 @roles = Role.sorted.select(&:consider_workflow?)
25 25 @trackers = Tracker.sorted
26 26 @workflow_counts = WorkflowTransition.group(:tracker_id, :role_id).count
27 27 end
28 28
29 29 def edit
30 30 find_trackers_roles_and_statuses_for_edit
31 31
32 32 if request.post? && @roles && @trackers && params[:transitions]
33 33 transitions = params[:transitions].deep_dup
34 34 transitions.each do |old_status_id, transitions_by_new_status|
35 35 transitions_by_new_status.each do |new_status_id, transition_by_rule|
36 36 transition_by_rule.reject! {|rule, transition| transition == 'no_change'}
37 37 end
38 38 end
39 39 WorkflowTransition.replace_transitions(@trackers, @roles, transitions)
40 40 flash[:notice] = l(:notice_successful_update)
41 41 redirect_to_referer_or workflows_edit_path
42 42 return
43 43 end
44 44
45 45 if @trackers && @roles && @statuses.any?
46 workflows = WorkflowTransition.where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id))
46 workflows = WorkflowTransition.
47 where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id)).
48 preload(:old_status, :new_status)
47 49 @workflows = {}
48 50 @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
49 51 @workflows['author'] = workflows.select {|w| w.author}
50 52 @workflows['assignee'] = workflows.select {|w| w.assignee}
51 53 end
52 54 end
53 55
54 56 def permissions
55 57 find_trackers_roles_and_statuses_for_edit
56 58
57 59 if request.post? && @roles && @trackers && params[:permissions]
58 60 permissions = params[:permissions].deep_dup
59 61 permissions.each { |field, rule_by_status_id|
60 62 rule_by_status_id.reject! {|status_id, rule| rule == 'no_change'}
61 63 }
62 64 WorkflowPermission.replace_permissions(@trackers, @roles, permissions)
63 65 flash[:notice] = l(:notice_successful_update)
64 66 redirect_to_referer_or workflows_permissions_path
65 67 return
66 68 end
67 69
68 70 if @roles && @trackers
69 71 @fields = (Tracker::CORE_FIELDS_ALL - @trackers.map(&:disabled_core_fields).reduce(:&)).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
70 72 @custom_fields = @trackers.map(&:custom_fields).flatten.uniq.sort
71 73 @permissions = WorkflowPermission.rules_by_status_id(@trackers, @roles)
72 74 @statuses.each {|status| @permissions[status.id] ||= {}}
73 75 end
74 76 end
75 77
76 78 def copy
77 79 @roles = Role.sorted.select(&:consider_workflow?)
78 80 @trackers = Tracker.sorted
79 81
80 82 if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
81 83 @source_tracker = nil
82 84 else
83 85 @source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
84 86 end
85 87 if params[:source_role_id].blank? || params[:source_role_id] == 'any'
86 88 @source_role = nil
87 89 else
88 90 @source_role = Role.find_by_id(params[:source_role_id].to_i)
89 91 end
90 92 @target_trackers = params[:target_tracker_ids].blank? ?
91 93 nil : Tracker.where(:id => params[:target_tracker_ids]).to_a
92 94 @target_roles = params[:target_role_ids].blank? ?
93 95 nil : Role.where(:id => params[:target_role_ids]).to_a
94 96 if request.post?
95 97 if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
96 98 flash.now[:error] = l(:error_workflow_copy_source)
97 99 elsif @target_trackers.blank? || @target_roles.blank?
98 100 flash.now[:error] = l(:error_workflow_copy_target)
99 101 else
100 102 WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
101 103 flash[:notice] = l(:notice_successful_update)
102 104 redirect_to workflows_copy_path(:source_tracker_id => @source_tracker, :source_role_id => @source_role)
103 105 end
104 106 end
105 107 end
106 108
107 109 private
108 110
109 111 def find_trackers_roles_and_statuses_for_edit
110 112 find_roles
111 113 find_trackers
112 114 find_statuses
113 115 end
114 116
115 117 def find_roles
116 118 ids = Array.wrap(params[:role_id])
117 119 if ids == ['all']
118 120 @roles = Role.sorted.to_a
119 121 elsif ids.present?
120 122 @roles = Role.where(:id => ids).to_a
121 123 end
122 124 @roles = nil if @roles.blank?
123 125 end
124 126
125 127 def find_trackers
126 128 ids = Array.wrap(params[:tracker_id])
127 129 if ids == ['all']
128 130 @trackers = Tracker.sorted.to_a
129 131 elsif ids.present?
130 132 @trackers = Tracker.where(:id => ids).to_a
131 133 end
132 134 @trackers = nil if @trackers.blank?
133 135 end
134 136
135 137 def find_statuses
136 138 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
137 139 if @trackers && @used_statuses_only
138 140 @statuses = @trackers.map(&:issue_statuses).flatten.uniq.sort.presence
139 141 end
140 142 @statuses ||= IssueStatus.sorted.to_a
141 143 end
142 144 end
@@ -1,95 +1,95
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WorkflowsHelper
21 21 def options_for_workflow_select(name, objects, selected, options={})
22 22 option_tags = ''.html_safe
23 23 multiple = false
24 24 if selected
25 25 if selected.size == objects.size
26 26 selected = 'all'
27 27 else
28 28 selected = selected.map(&:id)
29 29 if selected.size > 1
30 30 multiple = true
31 31 end
32 32 end
33 33 else
34 34 selected = objects.first.try(:id)
35 35 end
36 36 all_tag_options = {:value => 'all', :selected => (selected == 'all')}
37 37 if multiple
38 38 all_tag_options.merge!(:style => "display:none;")
39 39 end
40 40 option_tags << content_tag('option', l(:label_all), all_tag_options)
41 41 option_tags << options_from_collection_for_select(objects, "id", "name", selected)
42 42 select_tag name, option_tags, {:multiple => multiple}.merge(options)
43 43 end
44 44
45 45 def field_required?(field)
46 46 field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
47 47 end
48 48
49 49 def field_permission_tag(permissions, status, field, roles)
50 50 name = field.is_a?(CustomField) ? field.id.to_s : field
51 51 options = [["", ""], [l(:label_readonly), "readonly"]]
52 52 options << [l(:label_required), "required"] unless field_required?(field)
53 53 html_options = {}
54 54
55 55 if perm = permissions[status.id][name]
56 56 if perm.uniq.size > 1 || perm.size < @roles.size * @trackers.size
57 57 options << [l(:label_no_change_option), "no_change"]
58 58 selected = 'no_change'
59 59 else
60 60 selected = perm.first
61 61 end
62 62 end
63 63
64 64 hidden = field.is_a?(CustomField) &&
65 65 !field.visible? &&
66 66 !roles.detect {|role| role.custom_fields.to_a.include?(field)}
67 67
68 68 if hidden
69 69 options[0][0] = l(:label_hidden)
70 70 selected = ''
71 71 html_options[:disabled] = true
72 72 end
73 73
74 74 select_tag("permissions[#{status.id}][#{name}]", options_for_select(options, selected), html_options)
75 75 end
76 76
77 77 def transition_tag(workflows, old_status, new_status, name)
78 w = workflows.select {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id}.size
78 w = workflows.select {|w| w.old_status == old_status && w.new_status == new_status}.size
79 79
80 tag_name = "transitions[#{ old_status.id }][#{new_status.id}][#{name}]"
80 tag_name = "transitions[#{ old_status.try(:id) || 0 }][#{new_status.id}][#{name}]"
81 81 if w == 0 || w == @roles.size * @trackers.size
82 82
83 83 hidden_field_tag(tag_name, "0", :id => nil) +
84 84 check_box_tag(tag_name, "1", w != 0,
85 :class => "old-status-#{old_status.id} new-status-#{new_status.id}")
85 :class => "old-status-#{old_status.try(:id) || 0} new-status-#{new_status.id}")
86 86 else
87 87 select_tag tag_name,
88 88 options_for_select([
89 89 [l(:general_text_Yes), "1"],
90 90 [l(:general_text_No), "0"],
91 91 [l(:label_no_change_option), "no_change"]
92 92 ], "no_change")
93 93 end
94 94 end
95 95 end
@@ -1,1650 +1,1653
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 include Redmine::Utils::DateCalculation
21 21 include Redmine::I18n
22 22 before_save :set_parent_id
23 23 include Redmine::NestedSet::IssueNestedSet
24 24
25 25 belongs_to :project
26 26 belongs_to :tracker
27 27 belongs_to :status, :class_name => 'IssueStatus'
28 28 belongs_to :author, :class_name => 'User'
29 29 belongs_to :assigned_to, :class_name => 'Principal'
30 30 belongs_to :fixed_version, :class_name => 'Version'
31 31 belongs_to :priority, :class_name => 'IssuePriority'
32 32 belongs_to :category, :class_name => 'IssueCategory'
33 33
34 34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
35 35 has_many :visible_journals,
36 36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
37 37 :class_name => 'Journal',
38 38 :as => :journalized
39 39
40 40 has_many :time_entries, :dependent => :destroy
41 41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
42 42
43 43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 45
46 46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 47 acts_as_customizable
48 48 acts_as_watchable
49 49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
50 50 :preload => [:project, :status, :tracker],
51 51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
52 52
53 53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56 56
57 57 acts_as_activity_provider :scope => preload(:project, :author, :tracker),
58 58 :author_key => :author_id
59 59
60 60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61 61
62 62 attr_reader :current_journal
63 63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64 64
65 65 validates_presence_of :subject, :project, :tracker
66 66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67 67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68 68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69 69
70 70 validates_length_of :subject, :maximum => 255
71 71 validates_inclusion_of :done_ratio, :in => 0..100
72 72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 73 validates :start_date, :date => true
74 74 validates :due_date, :date => true
75 75 validate :validate_issue, :validate_required_fields
76 76 attr_protected :id
77 77
78 78 scope :visible, lambda {|*args|
79 79 joins(:project).
80 80 where(Issue.visible_condition(args.shift || User.current, *args))
81 81 }
82 82
83 83 scope :open, lambda {|*args|
84 84 is_closed = args.size > 0 ? !args.first : false
85 85 joins(:status).
86 86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
87 87 }
88 88
89 89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
90 90 scope :on_active_project, lambda {
91 91 joins(:project).
92 92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
93 93 }
94 94 scope :fixed_version, lambda {|versions|
95 95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
96 96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
97 97 }
98 98
99 99 before_validation :clear_disabled_fields
100 100 before_create :default_assign
101 101 before_save :close_duplicates, :update_done_ratio_from_issue_status,
102 102 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
103 103 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
104 104 after_save :reschedule_following_issues, :update_nested_set_attributes,
105 105 :update_parent_attributes, :create_journal
106 106 # Should be after_create but would be called before previous after_save callbacks
107 107 after_save :after_create_from_copy
108 108 after_destroy :update_parent_attributes
109 109 after_create :send_notification
110 110 # Keep it at the end of after_save callbacks
111 111 after_save :clear_assigned_to_was
112 112
113 113 # Returns a SQL conditions string used to find all issues visible by the specified user
114 114 def self.visible_condition(user, options={})
115 115 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
116 116 if user.id && user.logged?
117 117 case role.issues_visibility
118 118 when 'all'
119 119 nil
120 120 when 'default'
121 121 user_ids = [user.id] + user.groups.map(&:id).compact
122 122 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
123 123 when 'own'
124 124 user_ids = [user.id] + user.groups.map(&:id).compact
125 125 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
126 126 else
127 127 '1=0'
128 128 end
129 129 else
130 130 "(#{table_name}.is_private = #{connection.quoted_false})"
131 131 end
132 132 end
133 133 end
134 134
135 135 # Returns true if usr or current user is allowed to view the issue
136 136 def visible?(usr=nil)
137 137 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
138 138 if user.logged?
139 139 case role.issues_visibility
140 140 when 'all'
141 141 true
142 142 when 'default'
143 143 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
144 144 when 'own'
145 145 self.author == user || user.is_or_belongs_to?(assigned_to)
146 146 else
147 147 false
148 148 end
149 149 else
150 150 !self.is_private?
151 151 end
152 152 end
153 153 end
154 154
155 155 # Returns true if user or current user is allowed to edit or add a note to the issue
156 156 def editable?(user=User.current)
157 157 attributes_editable?(user) || user.allowed_to?(:add_issue_notes, project)
158 158 end
159 159
160 160 # Returns true if user or current user is allowed to edit the issue
161 161 def attributes_editable?(user=User.current)
162 162 user.allowed_to?(:edit_issues, project)
163 163 end
164 164
165 165 def initialize(attributes=nil, *args)
166 166 super
167 167 if new_record?
168 168 # set default values for new records only
169 169 self.priority ||= IssuePriority.default
170 170 self.watcher_user_ids = []
171 171 end
172 172 end
173 173
174 174 def create_or_update
175 175 super
176 176 ensure
177 177 @status_was = nil
178 178 end
179 179 private :create_or_update
180 180
181 181 # AR#Persistence#destroy would raise and RecordNotFound exception
182 182 # if the issue was already deleted or updated (non matching lock_version).
183 183 # This is a problem when bulk deleting issues or deleting a project
184 184 # (because an issue may already be deleted if its parent was deleted
185 185 # first).
186 186 # The issue is reloaded by the nested_set before being deleted so
187 187 # the lock_version condition should not be an issue but we handle it.
188 188 def destroy
189 189 super
190 190 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
191 191 # Stale or already deleted
192 192 begin
193 193 reload
194 194 rescue ActiveRecord::RecordNotFound
195 195 # The issue was actually already deleted
196 196 @destroyed = true
197 197 return freeze
198 198 end
199 199 # The issue was stale, retry to destroy
200 200 super
201 201 end
202 202
203 203 alias :base_reload :reload
204 204 def reload(*args)
205 205 @workflow_rule_by_attribute = nil
206 206 @assignable_versions = nil
207 207 @relations = nil
208 208 @spent_hours = nil
209 209 @total_spent_hours = nil
210 210 @total_estimated_hours = nil
211 211 base_reload(*args)
212 212 end
213 213
214 214 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
215 215 def available_custom_fields
216 216 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
217 217 end
218 218
219 219 def visible_custom_field_values(user=nil)
220 220 user_real = user || User.current
221 221 custom_field_values.select do |value|
222 222 value.custom_field.visible_by?(project, user_real)
223 223 end
224 224 end
225 225
226 226 # Copies attributes from another issue, arg can be an id or an Issue
227 227 def copy_from(arg, options={})
228 228 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
229 229 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
230 230 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
231 231 self.status = issue.status
232 232 self.author = User.current
233 233 unless options[:attachments] == false
234 234 self.attachments = issue.attachments.map do |attachement|
235 235 attachement.copy(:container => self)
236 236 end
237 237 end
238 238 @copied_from = issue
239 239 @copy_options = options
240 240 self
241 241 end
242 242
243 243 # Returns an unsaved copy of the issue
244 244 def copy(attributes=nil, copy_options={})
245 245 copy = self.class.new.copy_from(self, copy_options)
246 246 copy.attributes = attributes if attributes
247 247 copy
248 248 end
249 249
250 250 # Returns true if the issue is a copy
251 251 def copy?
252 252 @copied_from.present?
253 253 end
254 254
255 255 def status_id=(status_id)
256 256 if status_id.to_s != self.status_id.to_s
257 257 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
258 258 end
259 259 self.status_id
260 260 end
261 261
262 262 # Sets the status.
263 263 def status=(status)
264 264 if status != self.status
265 265 @workflow_rule_by_attribute = nil
266 266 end
267 267 association(:status).writer(status)
268 268 end
269 269
270 270 def priority_id=(pid)
271 271 self.priority = nil
272 272 write_attribute(:priority_id, pid)
273 273 end
274 274
275 275 def category_id=(cid)
276 276 self.category = nil
277 277 write_attribute(:category_id, cid)
278 278 end
279 279
280 280 def fixed_version_id=(vid)
281 281 self.fixed_version = nil
282 282 write_attribute(:fixed_version_id, vid)
283 283 end
284 284
285 285 def tracker_id=(tracker_id)
286 286 if tracker_id.to_s != self.tracker_id.to_s
287 287 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
288 288 end
289 289 self.tracker_id
290 290 end
291 291
292 292 # Sets the tracker.
293 293 # This will set the status to the default status of the new tracker if:
294 294 # * the status was the default for the previous tracker
295 295 # * or if the status was not part of the new tracker statuses
296 296 # * or the status was nil
297 297 def tracker=(tracker)
298 298 if tracker != self.tracker
299 299 if status == default_status
300 300 self.status = nil
301 301 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
302 302 self.status = nil
303 303 end
304 304 @custom_field_values = nil
305 305 @workflow_rule_by_attribute = nil
306 306 end
307 307 association(:tracker).writer(tracker)
308 308 self.status ||= default_status
309 309 self.tracker
310 310 end
311 311
312 312 def project_id=(project_id)
313 313 if project_id.to_s != self.project_id.to_s
314 314 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
315 315 end
316 316 self.project_id
317 317 end
318 318
319 319 # Sets the project.
320 320 # Unless keep_tracker argument is set to true, this will change the tracker
321 321 # to the first tracker of the new project if the previous tracker is not part
322 322 # of the new project trackers.
323 323 # This will clear the fixed_version is it's no longer valid for the new project.
324 324 # This will clear the parent issue if it's no longer valid for the new project.
325 325 # This will set the category to the category with the same name in the new
326 326 # project if it exists, or clear it if it doesn't.
327 327 def project=(project, keep_tracker=false)
328 328 project_was = self.project
329 329 association(:project).writer(project)
330 330 if project_was && project && project_was != project
331 331 @assignable_versions = nil
332 332
333 333 unless keep_tracker || project.trackers.include?(tracker)
334 334 self.tracker = project.trackers.first
335 335 end
336 336 # Reassign to the category with same name if any
337 337 if category
338 338 self.category = project.issue_categories.find_by_name(category.name)
339 339 end
340 340 # Keep the fixed_version if it's still valid in the new_project
341 341 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
342 342 self.fixed_version = nil
343 343 end
344 344 # Clear the parent task if it's no longer valid
345 345 unless valid_parent_project?
346 346 self.parent_issue_id = nil
347 347 end
348 348 @custom_field_values = nil
349 349 @workflow_rule_by_attribute = nil
350 350 end
351 351 self.project
352 352 end
353 353
354 354 def description=(arg)
355 355 if arg.is_a?(String)
356 356 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
357 357 end
358 358 write_attribute(:description, arg)
359 359 end
360 360
361 361 # Overrides assign_attributes so that project and tracker get assigned first
362 362 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
363 363 return if new_attributes.nil?
364 364 attrs = new_attributes.dup
365 365 attrs.stringify_keys!
366 366
367 367 %w(project project_id tracker tracker_id).each do |attr|
368 368 if attrs.has_key?(attr)
369 369 send "#{attr}=", attrs.delete(attr)
370 370 end
371 371 end
372 372 send :assign_attributes_without_project_and_tracker_first, attrs, *args
373 373 end
374 374 # Do not redefine alias chain on reload (see #4838)
375 375 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
376 376
377 377 def attributes=(new_attributes)
378 378 assign_attributes new_attributes
379 379 end
380 380
381 381 def estimated_hours=(h)
382 382 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
383 383 end
384 384
385 385 safe_attributes 'project_id',
386 386 'tracker_id',
387 387 'status_id',
388 388 'category_id',
389 389 'assigned_to_id',
390 390 'priority_id',
391 391 'fixed_version_id',
392 392 'subject',
393 393 'description',
394 394 'start_date',
395 395 'due_date',
396 396 'done_ratio',
397 397 'estimated_hours',
398 398 'custom_field_values',
399 399 'custom_fields',
400 400 'lock_version',
401 401 'notes',
402 402 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
403 403
404 404 safe_attributes 'notes',
405 405 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
406 406
407 407 safe_attributes 'private_notes',
408 408 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
409 409
410 410 safe_attributes 'watcher_user_ids',
411 411 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
412 412
413 413 safe_attributes 'is_private',
414 414 :if => lambda {|issue, user|
415 415 user.allowed_to?(:set_issues_private, issue.project) ||
416 416 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
417 417 }
418 418
419 419 safe_attributes 'parent_issue_id',
420 420 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
421 421 user.allowed_to?(:manage_subtasks, issue.project)}
422 422
423 423 def safe_attribute_names(user=nil)
424 424 names = super
425 425 names -= disabled_core_fields
426 426 names -= read_only_attribute_names(user)
427 427 if new_record?
428 428 # Make sure that project_id can always be set for new issues
429 429 names |= %w(project_id)
430 430 end
431 431 if dates_derived?
432 432 names -= %w(start_date due_date)
433 433 end
434 434 if priority_derived?
435 435 names -= %w(priority_id)
436 436 end
437 437 if done_ratio_derived?
438 438 names -= %w(done_ratio)
439 439 end
440 440 names
441 441 end
442 442
443 443 # Safely sets attributes
444 444 # Should be called from controllers instead of #attributes=
445 445 # attr_accessible is too rough because we still want things like
446 446 # Issue.new(:project => foo) to work
447 447 def safe_attributes=(attrs, user=User.current)
448 448 return unless attrs.is_a?(Hash)
449 449
450 450 attrs = attrs.deep_dup
451 451
452 452 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
453 453 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
454 454 if allowed_target_projects(user).where(:id => p.to_i).exists?
455 455 self.project_id = p
456 456 end
457 457 end
458 458
459 459 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
460 460 self.tracker_id = t
461 461 end
462 462 if project
463 463 # Set the default tracker to accept custom field values
464 464 # even if tracker is not specified
465 465 self.tracker ||= project.trackers.first
466 466 end
467 467
468 statuses_allowed = new_statuses_allowed_to(user)
468 469 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
469 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
470 if statuses_allowed.collect(&:id).include?(s.to_i)
470 471 self.status_id = s
471 472 end
472 473 end
474 if new_record? && !statuses_allowed.include?(status)
475 self.status = statuses_allowed.first || default_status
476 end
473 477
474 478 attrs = delete_unsafe_attributes(attrs, user)
475 479 return if attrs.empty?
476 480
477 481 if attrs['parent_issue_id'].present?
478 482 s = attrs['parent_issue_id'].to_s
479 483 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
480 484 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
481 485 end
482 486 end
483 487
484 488 if attrs['custom_field_values'].present?
485 489 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
486 490 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
487 491 end
488 492
489 493 if attrs['custom_fields'].present?
490 494 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
491 495 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
492 496 end
493 497
494 498 # mass-assignment security bypass
495 499 assign_attributes attrs, :without_protection => true
496 500 end
497 501
498 502 def disabled_core_fields
499 503 tracker ? tracker.disabled_core_fields : []
500 504 end
501 505
502 506 # Returns the custom_field_values that can be edited by the given user
503 507 def editable_custom_field_values(user=nil)
504 508 visible_custom_field_values(user).reject do |value|
505 509 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
506 510 end
507 511 end
508 512
509 513 # Returns the custom fields that can be edited by the given user
510 514 def editable_custom_fields(user=nil)
511 515 editable_custom_field_values(user).map(&:custom_field).uniq
512 516 end
513 517
514 518 # Returns the names of attributes that are read-only for user or the current user
515 519 # For users with multiple roles, the read-only fields are the intersection of
516 520 # read-only fields of each role
517 521 # The result is an array of strings where sustom fields are represented with their ids
518 522 #
519 523 # Examples:
520 524 # issue.read_only_attribute_names # => ['due_date', '2']
521 525 # issue.read_only_attribute_names(user) # => []
522 526 def read_only_attribute_names(user=nil)
523 527 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
524 528 end
525 529
526 530 # Returns the names of required attributes for user or the current user
527 531 # For users with multiple roles, the required fields are the intersection of
528 532 # required fields of each role
529 533 # The result is an array of strings where sustom fields are represented with their ids
530 534 #
531 535 # Examples:
532 536 # issue.required_attribute_names # => ['due_date', '2']
533 537 # issue.required_attribute_names(user) # => []
534 538 def required_attribute_names(user=nil)
535 539 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
536 540 end
537 541
538 542 # Returns true if the attribute is required for user
539 543 def required_attribute?(name, user=nil)
540 544 required_attribute_names(user).include?(name.to_s)
541 545 end
542 546
543 547 # Returns a hash of the workflow rule by attribute for the given user
544 548 #
545 549 # Examples:
546 550 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
547 551 def workflow_rule_by_attribute(user=nil)
548 552 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
549 553
550 554 user_real = user || User.current
551 555 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
552 556 roles = roles.select(&:consider_workflow?)
553 557 return {} if roles.empty?
554 558
555 559 result = {}
556 560 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
557 561 if workflow_permissions.any?
558 562 workflow_rules = workflow_permissions.inject({}) do |h, wp|
559 563 h[wp.field_name] ||= {}
560 564 h[wp.field_name][wp.role_id] = wp.rule
561 565 h
562 566 end
563 567 fields_with_roles = {}
564 568 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
565 569 fields_with_roles[field_id] ||= []
566 570 fields_with_roles[field_id] << role_id
567 571 end
568 572 roles.each do |role|
569 573 fields_with_roles.each do |field_id, role_ids|
570 574 unless role_ids.include?(role.id)
571 575 field_name = field_id.to_s
572 576 workflow_rules[field_name] ||= {}
573 577 workflow_rules[field_name][role.id] = 'readonly'
574 578 end
575 579 end
576 580 end
577 581 workflow_rules.each do |attr, rules|
578 582 next if rules.size < roles.size
579 583 uniq_rules = rules.values.uniq
580 584 if uniq_rules.size == 1
581 585 result[attr] = uniq_rules.first
582 586 else
583 587 result[attr] = 'required'
584 588 end
585 589 end
586 590 end
587 591 @workflow_rule_by_attribute = result if user.nil?
588 592 result
589 593 end
590 594 private :workflow_rule_by_attribute
591 595
592 596 def done_ratio
593 597 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
594 598 status.default_done_ratio
595 599 else
596 600 read_attribute(:done_ratio)
597 601 end
598 602 end
599 603
600 604 def self.use_status_for_done_ratio?
601 605 Setting.issue_done_ratio == 'issue_status'
602 606 end
603 607
604 608 def self.use_field_for_done_ratio?
605 609 Setting.issue_done_ratio == 'issue_field'
606 610 end
607 611
608 612 def validate_issue
609 613 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
610 614 errors.add :due_date, :greater_than_start_date
611 615 end
612 616
613 617 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
614 618 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
615 619 end
616 620
617 621 if fixed_version
618 622 if !assignable_versions.include?(fixed_version)
619 623 errors.add :fixed_version_id, :inclusion
620 624 elsif reopening? && fixed_version.closed?
621 625 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
622 626 end
623 627 end
624 628
625 629 # Checks that the issue can not be added/moved to a disabled tracker
626 630 if project && (tracker_id_changed? || project_id_changed?)
627 631 unless project.trackers.include?(tracker)
628 632 errors.add :tracker_id, :inclusion
629 633 end
630 634 end
631 635
632 636 # Checks parent issue assignment
633 637 if @invalid_parent_issue_id.present?
634 638 errors.add :parent_issue_id, :invalid
635 639 elsif @parent_issue
636 640 if !valid_parent_project?(@parent_issue)
637 641 errors.add :parent_issue_id, :invalid
638 642 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
639 643 errors.add :parent_issue_id, :invalid
640 644 elsif !new_record?
641 645 # moving an existing issue
642 646 if move_possible?(@parent_issue)
643 647 # move accepted
644 648 else
645 649 errors.add :parent_issue_id, :invalid
646 650 end
647 651 end
648 652 end
649 653 end
650 654
651 655 # Validates the issue against additional workflow requirements
652 656 def validate_required_fields
653 657 user = new_record? ? author : current_journal.try(:user)
654 658
655 659 required_attribute_names(user).each do |attribute|
656 660 if attribute =~ /^\d+$/
657 661 attribute = attribute.to_i
658 662 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
659 663 if v && v.value.blank?
660 664 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
661 665 end
662 666 else
663 667 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
664 668 errors.add attribute, :blank
665 669 end
666 670 end
667 671 end
668 672 end
669 673
670 674 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
671 675 # so that custom values that are not editable are not validated (eg. a custom field that
672 676 # is marked as required should not trigger a validation error if the user is not allowed
673 677 # to edit this field).
674 678 def validate_custom_field_values
675 679 user = new_record? ? author : current_journal.try(:user)
676 680 if new_record? || custom_field_values_changed?
677 681 editable_custom_field_values(user).each(&:validate_value)
678 682 end
679 683 end
680 684
681 685 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
682 686 # even if the user turns off the setting later
683 687 def update_done_ratio_from_issue_status
684 688 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
685 689 self.done_ratio = status.default_done_ratio
686 690 end
687 691 end
688 692
689 693 def init_journal(user, notes = "")
690 694 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
691 695 end
692 696
693 697 # Returns the current journal or nil if it's not initialized
694 698 def current_journal
695 699 @current_journal
696 700 end
697 701
698 702 # Returns the names of attributes that are journalized when updating the issue
699 703 def journalized_attribute_names
700 704 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
701 705 if tracker
702 706 names -= tracker.disabled_core_fields
703 707 end
704 708 names
705 709 end
706 710
707 711 # Returns the id of the last journal or nil
708 712 def last_journal_id
709 713 if new_record?
710 714 nil
711 715 else
712 716 journals.maximum(:id)
713 717 end
714 718 end
715 719
716 720 # Returns a scope for journals that have an id greater than journal_id
717 721 def journals_after(journal_id)
718 722 scope = journals.reorder("#{Journal.table_name}.id ASC")
719 723 if journal_id.present?
720 724 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
721 725 end
722 726 scope
723 727 end
724 728
725 729 # Returns the initial status of the issue
726 730 # Returns nil for a new issue
727 731 def status_was
728 732 if status_id_changed?
729 733 if status_id_was.to_i > 0
730 734 @status_was ||= IssueStatus.find_by_id(status_id_was)
731 735 end
732 736 else
733 737 @status_was ||= status
734 738 end
735 739 end
736 740
737 741 # Return true if the issue is closed, otherwise false
738 742 def closed?
739 743 status.present? && status.is_closed?
740 744 end
741 745
742 746 # Returns true if the issue was closed when loaded
743 747 def was_closed?
744 748 status_was.present? && status_was.is_closed?
745 749 end
746 750
747 751 # Return true if the issue is being reopened
748 752 def reopening?
749 753 if new_record?
750 754 false
751 755 else
752 756 status_id_changed? && !closed? && was_closed?
753 757 end
754 758 end
755 759 alias :reopened? :reopening?
756 760
757 761 # Return true if the issue is being closed
758 762 def closing?
759 763 if new_record?
760 764 closed?
761 765 else
762 766 status_id_changed? && closed? && !was_closed?
763 767 end
764 768 end
765 769
766 770 # Returns true if the issue is overdue
767 771 def overdue?
768 772 due_date.present? && (due_date < Date.today) && !closed?
769 773 end
770 774
771 775 # Is the amount of work done less than it should for the due date
772 776 def behind_schedule?
773 777 return false if start_date.nil? || due_date.nil?
774 778 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
775 779 return done_date <= Date.today
776 780 end
777 781
778 782 # Does this issue have children?
779 783 def children?
780 784 !leaf?
781 785 end
782 786
783 787 # Users the issue can be assigned to
784 788 def assignable_users
785 789 users = project.assignable_users.to_a
786 790 users << author if author
787 791 users << assigned_to if assigned_to
788 792 users.uniq.sort
789 793 end
790 794
791 795 # Versions that the issue can be assigned to
792 796 def assignable_versions
793 797 return @assignable_versions if @assignable_versions
794 798
795 799 versions = project.shared_versions.open.to_a
796 800 if fixed_version
797 801 if fixed_version_id_changed?
798 802 # nothing to do
799 803 elsif project_id_changed?
800 804 if project.shared_versions.include?(fixed_version)
801 805 versions << fixed_version
802 806 end
803 807 else
804 808 versions << fixed_version
805 809 end
806 810 end
807 811 @assignable_versions = versions.uniq.sort
808 812 end
809 813
810 814 # Returns true if this issue is blocked by another issue that is still open
811 815 def blocked?
812 816 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
813 817 end
814 818
815 819 # Returns the default status of the issue based on its tracker
816 820 # Returns nil if tracker is nil
817 821 def default_status
818 822 tracker.try(:default_status)
819 823 end
820 824
821 825 # Returns an array of statuses that user is able to apply
822 826 def new_statuses_allowed_to(user=User.current, include_default=false)
823 827 if new_record? && @copied_from
824 828 [default_status, @copied_from.status].compact.uniq.sort
825 829 else
826 830 initial_status = nil
827 831 if new_record?
828 initial_status = default_status
832 # nop
829 833 elsif tracker_id_changed?
830 834 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
831 835 initial_status = default_status
832 836 elsif tracker.issue_status_ids.include?(status_id_was)
833 837 initial_status = IssueStatus.find_by_id(status_id_was)
834 838 else
835 839 initial_status = default_status
836 840 end
837 841 else
838 842 initial_status = status_was
839 843 end
840 844
841 845 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
842 846 assignee_transitions_allowed = initial_assigned_to_id.present? &&
843 847 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
844 848
845 849 statuses = []
846 if initial_status
847 statuses += initial_status.find_new_statuses_allowed_to(
848 user.admin ? Role.all.to_a : user.roles_for_project(project),
849 tracker,
850 author == user,
851 assignee_transitions_allowed
852 )
853 end
850 statuses += IssueStatus.new_statuses_allowed(
851 initial_status,
852 user.admin ? Role.all.to_a : user.roles_for_project(project),
853 tracker,
854 author == user,
855 assignee_transitions_allowed
856 )
854 857 statuses << initial_status unless statuses.empty?
855 statuses << default_status if include_default
858 statuses << default_status if include_default || (new_record? && statuses.empty?)
856 859 statuses = statuses.compact.uniq.sort
857 860 if blocked?
858 861 statuses.reject!(&:is_closed?)
859 862 end
860 863 statuses
861 864 end
862 865 end
863 866
864 867 # Returns the previous assignee (user or group) if changed
865 868 def assigned_to_was
866 869 # assigned_to_id_was is reset before after_save callbacks
867 870 user_id = @previous_assigned_to_id || assigned_to_id_was
868 871 if user_id && user_id != assigned_to_id
869 872 @assigned_to_was ||= Principal.find_by_id(user_id)
870 873 end
871 874 end
872 875
873 876 # Returns the users that should be notified
874 877 def notified_users
875 878 notified = []
876 879 # Author and assignee are always notified unless they have been
877 880 # locked or don't want to be notified
878 881 notified << author if author
879 882 if assigned_to
880 883 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
881 884 end
882 885 if assigned_to_was
883 886 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
884 887 end
885 888 notified = notified.select {|u| u.active? && u.notify_about?(self)}
886 889
887 890 notified += project.notified_users
888 891 notified.uniq!
889 892 # Remove users that can not view the issue
890 893 notified.reject! {|user| !visible?(user)}
891 894 notified
892 895 end
893 896
894 897 # Returns the email addresses that should be notified
895 898 def recipients
896 899 notified_users.collect(&:mail)
897 900 end
898 901
899 902 def each_notification(users, &block)
900 903 if users.any?
901 904 if custom_field_values.detect {|value| !value.custom_field.visible?}
902 905 users_by_custom_field_visibility = users.group_by do |user|
903 906 visible_custom_field_values(user).map(&:custom_field_id).sort
904 907 end
905 908 users_by_custom_field_visibility.values.each do |users|
906 909 yield(users)
907 910 end
908 911 else
909 912 yield(users)
910 913 end
911 914 end
912 915 end
913 916
914 917 # Returns the number of hours spent on this issue
915 918 def spent_hours
916 919 @spent_hours ||= time_entries.sum(:hours) || 0
917 920 end
918 921
919 922 # Returns the total number of hours spent on this issue and its descendants
920 923 def total_spent_hours
921 924 @total_spent_hours ||= if leaf?
922 925 spent_hours
923 926 else
924 927 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
925 928 end
926 929 end
927 930
928 931 def total_estimated_hours
929 932 if leaf?
930 933 estimated_hours
931 934 else
932 935 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
933 936 end
934 937 end
935 938
936 939 def relations
937 940 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
938 941 end
939 942
940 943 # Preloads relations for a collection of issues
941 944 def self.load_relations(issues)
942 945 if issues.any?
943 946 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
944 947 issues.each do |issue|
945 948 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
946 949 end
947 950 end
948 951 end
949 952
950 953 # Preloads visible spent time for a collection of issues
951 954 def self.load_visible_spent_hours(issues, user=User.current)
952 955 if issues.any?
953 956 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
954 957 issues.each do |issue|
955 958 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
956 959 end
957 960 end
958 961 end
959 962
960 963 # Preloads visible total spent time for a collection of issues
961 964 def self.load_visible_total_spent_hours(issues, user=User.current)
962 965 if issues.any?
963 966 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
964 967 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
965 968 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
966 969 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
967 970 issues.each do |issue|
968 971 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
969 972 end
970 973 end
971 974 end
972 975
973 976 # Preloads visible relations for a collection of issues
974 977 def self.load_visible_relations(issues, user=User.current)
975 978 if issues.any?
976 979 issue_ids = issues.map(&:id)
977 980 # Relations with issue_from in given issues and visible issue_to
978 981 relations_from = IssueRelation.joins(:issue_to => :project).
979 982 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
980 983 # Relations with issue_to in given issues and visible issue_from
981 984 relations_to = IssueRelation.joins(:issue_from => :project).
982 985 where(visible_condition(user)).
983 986 where(:issue_to_id => issue_ids).to_a
984 987 issues.each do |issue|
985 988 relations =
986 989 relations_from.select {|relation| relation.issue_from_id == issue.id} +
987 990 relations_to.select {|relation| relation.issue_to_id == issue.id}
988 991
989 992 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
990 993 end
991 994 end
992 995 end
993 996
994 997 # Finds an issue relation given its id.
995 998 def find_relation(relation_id)
996 999 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
997 1000 end
998 1001
999 1002 # Returns all the other issues that depend on the issue
1000 1003 # The algorithm is a modified breadth first search (bfs)
1001 1004 def all_dependent_issues(except=[])
1002 1005 # The found dependencies
1003 1006 dependencies = []
1004 1007
1005 1008 # The visited flag for every node (issue) used by the breadth first search
1006 1009 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
1007 1010
1008 1011 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
1009 1012 # the issue when it is processed.
1010 1013
1011 1014 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
1012 1015 # but its children will not be added to the queue when it is processed.
1013 1016
1014 1017 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
1015 1018 # the queue, but its children have not been added.
1016 1019
1017 1020 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
1018 1021 # the children still need to be processed.
1019 1022
1020 1023 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
1021 1024 # added as dependent issues. It needs no further processing.
1022 1025
1023 1026 issue_status = Hash.new(eNOT_DISCOVERED)
1024 1027
1025 1028 # The queue
1026 1029 queue = []
1027 1030
1028 1031 # Initialize the bfs, add start node (self) to the queue
1029 1032 queue << self
1030 1033 issue_status[self] = ePROCESS_ALL
1031 1034
1032 1035 while (!queue.empty?) do
1033 1036 current_issue = queue.shift
1034 1037 current_issue_status = issue_status[current_issue]
1035 1038 dependencies << current_issue
1036 1039
1037 1040 # Add parent to queue, if not already in it.
1038 1041 parent = current_issue.parent
1039 1042 parent_status = issue_status[parent]
1040 1043
1041 1044 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
1042 1045 queue << parent
1043 1046 issue_status[parent] = ePROCESS_RELATIONS_ONLY
1044 1047 end
1045 1048
1046 1049 # Add children to queue, but only if they are not already in it and
1047 1050 # the children of the current node need to be processed.
1048 1051 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
1049 1052 current_issue.children.each do |child|
1050 1053 next if except.include?(child)
1051 1054
1052 1055 if (issue_status[child] == eNOT_DISCOVERED)
1053 1056 queue << child
1054 1057 issue_status[child] = ePROCESS_ALL
1055 1058 elsif (issue_status[child] == eRELATIONS_PROCESSED)
1056 1059 queue << child
1057 1060 issue_status[child] = ePROCESS_CHILDREN_ONLY
1058 1061 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
1059 1062 queue << child
1060 1063 issue_status[child] = ePROCESS_ALL
1061 1064 end
1062 1065 end
1063 1066 end
1064 1067
1065 1068 # Add related issues to the queue, if they are not already in it.
1066 1069 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1067 1070 next if except.include?(related_issue)
1068 1071
1069 1072 if (issue_status[related_issue] == eNOT_DISCOVERED)
1070 1073 queue << related_issue
1071 1074 issue_status[related_issue] = ePROCESS_ALL
1072 1075 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1073 1076 queue << related_issue
1074 1077 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1075 1078 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1076 1079 queue << related_issue
1077 1080 issue_status[related_issue] = ePROCESS_ALL
1078 1081 end
1079 1082 end
1080 1083
1081 1084 # Set new status for current issue
1082 1085 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1083 1086 issue_status[current_issue] = eALL_PROCESSED
1084 1087 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1085 1088 issue_status[current_issue] = eRELATIONS_PROCESSED
1086 1089 end
1087 1090 end # while
1088 1091
1089 1092 # Remove the issues from the "except" parameter from the result array
1090 1093 dependencies -= except
1091 1094 dependencies.delete(self)
1092 1095
1093 1096 dependencies
1094 1097 end
1095 1098
1096 1099 # Returns an array of issues that duplicate this one
1097 1100 def duplicates
1098 1101 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1099 1102 end
1100 1103
1101 1104 # Returns the due date or the target due date if any
1102 1105 # Used on gantt chart
1103 1106 def due_before
1104 1107 due_date || (fixed_version ? fixed_version.effective_date : nil)
1105 1108 end
1106 1109
1107 1110 # Returns the time scheduled for this issue.
1108 1111 #
1109 1112 # Example:
1110 1113 # Start Date: 2/26/09, End Date: 3/04/09
1111 1114 # duration => 6
1112 1115 def duration
1113 1116 (start_date && due_date) ? due_date - start_date : 0
1114 1117 end
1115 1118
1116 1119 # Returns the duration in working days
1117 1120 def working_duration
1118 1121 (start_date && due_date) ? working_days(start_date, due_date) : 0
1119 1122 end
1120 1123
1121 1124 def soonest_start(reload=false)
1122 1125 if @soonest_start.nil? || reload
1123 1126 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1124 1127 p = @parent_issue || parent
1125 1128 if p && Setting.parent_issue_dates == 'derived'
1126 1129 dates << p.soonest_start
1127 1130 end
1128 1131 @soonest_start = dates.compact.max
1129 1132 end
1130 1133 @soonest_start
1131 1134 end
1132 1135
1133 1136 # Sets start_date on the given date or the next working day
1134 1137 # and changes due_date to keep the same working duration.
1135 1138 def reschedule_on(date)
1136 1139 wd = working_duration
1137 1140 date = next_working_date(date)
1138 1141 self.start_date = date
1139 1142 self.due_date = add_working_days(date, wd)
1140 1143 end
1141 1144
1142 1145 # Reschedules the issue on the given date or the next working day and saves the record.
1143 1146 # If the issue is a parent task, this is done by rescheduling its subtasks.
1144 1147 def reschedule_on!(date)
1145 1148 return if date.nil?
1146 1149 if leaf? || !dates_derived?
1147 1150 if start_date.nil? || start_date != date
1148 1151 if start_date && start_date > date
1149 1152 # Issue can not be moved earlier than its soonest start date
1150 1153 date = [soonest_start(true), date].compact.max
1151 1154 end
1152 1155 reschedule_on(date)
1153 1156 begin
1154 1157 save
1155 1158 rescue ActiveRecord::StaleObjectError
1156 1159 reload
1157 1160 reschedule_on(date)
1158 1161 save
1159 1162 end
1160 1163 end
1161 1164 else
1162 1165 leaves.each do |leaf|
1163 1166 if leaf.start_date
1164 1167 # Only move subtask if it starts at the same date as the parent
1165 1168 # or if it starts before the given date
1166 1169 if start_date == leaf.start_date || date > leaf.start_date
1167 1170 leaf.reschedule_on!(date)
1168 1171 end
1169 1172 else
1170 1173 leaf.reschedule_on!(date)
1171 1174 end
1172 1175 end
1173 1176 end
1174 1177 end
1175 1178
1176 1179 def dates_derived?
1177 1180 !leaf? && Setting.parent_issue_dates == 'derived'
1178 1181 end
1179 1182
1180 1183 def priority_derived?
1181 1184 !leaf? && Setting.parent_issue_priority == 'derived'
1182 1185 end
1183 1186
1184 1187 def done_ratio_derived?
1185 1188 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1186 1189 end
1187 1190
1188 1191 def <=>(issue)
1189 1192 if issue.nil?
1190 1193 -1
1191 1194 elsif root_id != issue.root_id
1192 1195 (root_id || 0) <=> (issue.root_id || 0)
1193 1196 else
1194 1197 (lft || 0) <=> (issue.lft || 0)
1195 1198 end
1196 1199 end
1197 1200
1198 1201 def to_s
1199 1202 "#{tracker} ##{id}: #{subject}"
1200 1203 end
1201 1204
1202 1205 # Returns a string of css classes that apply to the issue
1203 1206 def css_classes(user=User.current)
1204 1207 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1205 1208 s << ' closed' if closed?
1206 1209 s << ' overdue' if overdue?
1207 1210 s << ' child' if child?
1208 1211 s << ' parent' unless leaf?
1209 1212 s << ' private' if is_private?
1210 1213 if user.logged?
1211 1214 s << ' created-by-me' if author_id == user.id
1212 1215 s << ' assigned-to-me' if assigned_to_id == user.id
1213 1216 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1214 1217 end
1215 1218 s
1216 1219 end
1217 1220
1218 1221 # Unassigns issues from +version+ if it's no longer shared with issue's project
1219 1222 def self.update_versions_from_sharing_change(version)
1220 1223 # Update issues assigned to the version
1221 1224 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1222 1225 end
1223 1226
1224 1227 # Unassigns issues from versions that are no longer shared
1225 1228 # after +project+ was moved
1226 1229 def self.update_versions_from_hierarchy_change(project)
1227 1230 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1228 1231 # Update issues of the moved projects and issues assigned to a version of a moved project
1229 1232 Issue.update_versions(
1230 1233 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1231 1234 moved_project_ids, moved_project_ids]
1232 1235 )
1233 1236 end
1234 1237
1235 1238 def parent_issue_id=(arg)
1236 1239 s = arg.to_s.strip.presence
1237 1240 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1238 1241 @invalid_parent_issue_id = nil
1239 1242 elsif s.blank?
1240 1243 @parent_issue = nil
1241 1244 @invalid_parent_issue_id = nil
1242 1245 else
1243 1246 @parent_issue = nil
1244 1247 @invalid_parent_issue_id = arg
1245 1248 end
1246 1249 end
1247 1250
1248 1251 def parent_issue_id
1249 1252 if @invalid_parent_issue_id
1250 1253 @invalid_parent_issue_id
1251 1254 elsif instance_variable_defined? :@parent_issue
1252 1255 @parent_issue.nil? ? nil : @parent_issue.id
1253 1256 else
1254 1257 parent_id
1255 1258 end
1256 1259 end
1257 1260
1258 1261 def set_parent_id
1259 1262 self.parent_id = parent_issue_id
1260 1263 end
1261 1264
1262 1265 # Returns true if issue's project is a valid
1263 1266 # parent issue project
1264 1267 def valid_parent_project?(issue=parent)
1265 1268 return true if issue.nil? || issue.project_id == project_id
1266 1269
1267 1270 case Setting.cross_project_subtasks
1268 1271 when 'system'
1269 1272 true
1270 1273 when 'tree'
1271 1274 issue.project.root == project.root
1272 1275 when 'hierarchy'
1273 1276 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1274 1277 when 'descendants'
1275 1278 issue.project.is_or_is_ancestor_of?(project)
1276 1279 else
1277 1280 false
1278 1281 end
1279 1282 end
1280 1283
1281 1284 # Returns an issue scope based on project and scope
1282 1285 def self.cross_project_scope(project, scope=nil)
1283 1286 if project.nil?
1284 1287 return Issue
1285 1288 end
1286 1289 case scope
1287 1290 when 'all', 'system'
1288 1291 Issue
1289 1292 when 'tree'
1290 1293 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1291 1294 :lft => project.root.lft, :rgt => project.root.rgt)
1292 1295 when 'hierarchy'
1293 1296 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1294 1297 :lft => project.lft, :rgt => project.rgt)
1295 1298 when 'descendants'
1296 1299 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1297 1300 :lft => project.lft, :rgt => project.rgt)
1298 1301 else
1299 1302 Issue.where(:project_id => project.id)
1300 1303 end
1301 1304 end
1302 1305
1303 1306 def self.by_tracker(project)
1304 1307 count_and_group_by(:project => project, :association => :tracker)
1305 1308 end
1306 1309
1307 1310 def self.by_version(project)
1308 1311 count_and_group_by(:project => project, :association => :fixed_version)
1309 1312 end
1310 1313
1311 1314 def self.by_priority(project)
1312 1315 count_and_group_by(:project => project, :association => :priority)
1313 1316 end
1314 1317
1315 1318 def self.by_category(project)
1316 1319 count_and_group_by(:project => project, :association => :category)
1317 1320 end
1318 1321
1319 1322 def self.by_assigned_to(project)
1320 1323 count_and_group_by(:project => project, :association => :assigned_to)
1321 1324 end
1322 1325
1323 1326 def self.by_author(project)
1324 1327 count_and_group_by(:project => project, :association => :author)
1325 1328 end
1326 1329
1327 1330 def self.by_subproject(project)
1328 1331 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1329 1332 r.reject {|r| r["project_id"] == project.id.to_s}
1330 1333 end
1331 1334
1332 1335 # Query generator for selecting groups of issue counts for a project
1333 1336 # based on specific criteria
1334 1337 #
1335 1338 # Options
1336 1339 # * project - Project to search in.
1337 1340 # * with_subprojects - Includes subprojects issues if set to true.
1338 1341 # * association - Symbol. Association for grouping.
1339 1342 def self.count_and_group_by(options)
1340 1343 assoc = reflect_on_association(options[:association])
1341 1344 select_field = assoc.foreign_key
1342 1345
1343 1346 Issue.
1344 1347 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1345 1348 joins(:status, assoc.name).
1346 1349 group(:status_id, :is_closed, select_field).
1347 1350 count.
1348 1351 map do |columns, total|
1349 1352 status_id, is_closed, field_value = columns
1350 1353 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1351 1354 {
1352 1355 "status_id" => status_id.to_s,
1353 1356 "closed" => is_closed,
1354 1357 select_field => field_value.to_s,
1355 1358 "total" => total.to_s
1356 1359 }
1357 1360 end
1358 1361 end
1359 1362
1360 1363 # Returns a scope of projects that user can assign the issue to
1361 1364 def allowed_target_projects(user=User.current)
1362 1365 current_project = new_record? ? nil : project
1363 1366 self.class.allowed_target_projects(user, current_project)
1364 1367 end
1365 1368
1366 1369 # Returns a scope of projects that user can assign issues to
1367 1370 # If current_project is given, it will be included in the scope
1368 1371 def self.allowed_target_projects(user=User.current, current_project=nil)
1369 1372 condition = Project.allowed_to_condition(user, :add_issues)
1370 1373 if current_project
1371 1374 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1372 1375 end
1373 1376 Project.where(condition)
1374 1377 end
1375 1378
1376 1379 private
1377 1380
1378 1381 def after_project_change
1379 1382 # Update project_id on related time entries
1380 1383 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1381 1384
1382 1385 # Delete issue relations
1383 1386 unless Setting.cross_project_issue_relations?
1384 1387 relations_from.clear
1385 1388 relations_to.clear
1386 1389 end
1387 1390
1388 1391 # Move subtasks that were in the same project
1389 1392 children.each do |child|
1390 1393 next unless child.project_id == project_id_was
1391 1394 # Change project and keep project
1392 1395 child.send :project=, project, true
1393 1396 unless child.save
1394 1397 raise ActiveRecord::Rollback
1395 1398 end
1396 1399 end
1397 1400 end
1398 1401
1399 1402 # Callback for after the creation of an issue by copy
1400 1403 # * adds a "copied to" relation with the copied issue
1401 1404 # * copies subtasks from the copied issue
1402 1405 def after_create_from_copy
1403 1406 return unless copy? && !@after_create_from_copy_handled
1404 1407
1405 1408 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1406 1409 if @current_journal
1407 1410 @copied_from.init_journal(@current_journal.user)
1408 1411 end
1409 1412 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1410 1413 unless relation.save
1411 1414 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1412 1415 end
1413 1416 end
1414 1417
1415 1418 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1416 1419 copy_options = (@copy_options || {}).merge(:subtasks => false)
1417 1420 copied_issue_ids = {@copied_from.id => self.id}
1418 1421 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1419 1422 # Do not copy self when copying an issue as a descendant of the copied issue
1420 1423 next if child == self
1421 1424 # Do not copy subtasks of issues that were not copied
1422 1425 next unless copied_issue_ids[child.parent_id]
1423 1426 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1424 1427 unless child.visible?
1425 1428 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1426 1429 next
1427 1430 end
1428 1431 copy = Issue.new.copy_from(child, copy_options)
1429 1432 if @current_journal
1430 1433 copy.init_journal(@current_journal.user)
1431 1434 end
1432 1435 copy.author = author
1433 1436 copy.project = project
1434 1437 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1435 1438 unless copy.save
1436 1439 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1437 1440 next
1438 1441 end
1439 1442 copied_issue_ids[child.id] = copy.id
1440 1443 end
1441 1444 end
1442 1445 @after_create_from_copy_handled = true
1443 1446 end
1444 1447
1445 1448 def update_nested_set_attributes
1446 1449 if parent_id_changed?
1447 1450 update_nested_set_attributes_on_parent_change
1448 1451 end
1449 1452 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1450 1453 end
1451 1454
1452 1455 # Updates the nested set for when an existing issue is moved
1453 1456 def update_nested_set_attributes_on_parent_change
1454 1457 former_parent_id = parent_id_was
1455 1458 # delete invalid relations of all descendants
1456 1459 self_and_descendants.each do |issue|
1457 1460 issue.relations.each do |relation|
1458 1461 relation.destroy unless relation.valid?
1459 1462 end
1460 1463 end
1461 1464 # update former parent
1462 1465 recalculate_attributes_for(former_parent_id) if former_parent_id
1463 1466 end
1464 1467
1465 1468 def update_parent_attributes
1466 1469 if parent_id
1467 1470 recalculate_attributes_for(parent_id)
1468 1471 association(:parent).reset
1469 1472 end
1470 1473 end
1471 1474
1472 1475 def recalculate_attributes_for(issue_id)
1473 1476 if issue_id && p = Issue.find_by_id(issue_id)
1474 1477 if p.priority_derived?
1475 1478 # priority = highest priority of children
1476 1479 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1477 1480 p.priority = IssuePriority.find_by_position(priority_position)
1478 1481 end
1479 1482 end
1480 1483
1481 1484 if p.dates_derived?
1482 1485 # start/due dates = lowest/highest dates of children
1483 1486 p.start_date = p.children.minimum(:start_date)
1484 1487 p.due_date = p.children.maximum(:due_date)
1485 1488 if p.start_date && p.due_date && p.due_date < p.start_date
1486 1489 p.start_date, p.due_date = p.due_date, p.start_date
1487 1490 end
1488 1491 end
1489 1492
1490 1493 if p.done_ratio_derived?
1491 1494 # done ratio = weighted average ratio of leaves
1492 1495 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1493 1496 leaves_count = p.leaves.count
1494 1497 if leaves_count > 0
1495 1498 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1496 1499 if average == 0
1497 1500 average = 1
1498 1501 end
1499 1502 done = p.leaves.joins(:status).
1500 1503 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1501 1504 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1502 1505 progress = done / (average * leaves_count)
1503 1506 p.done_ratio = progress.round
1504 1507 end
1505 1508 end
1506 1509 end
1507 1510
1508 1511 # ancestors will be recursively updated
1509 1512 p.save(:validate => false)
1510 1513 end
1511 1514 end
1512 1515
1513 1516 # Update issues so their versions are not pointing to a
1514 1517 # fixed_version that is not shared with the issue's project
1515 1518 def self.update_versions(conditions=nil)
1516 1519 # Only need to update issues with a fixed_version from
1517 1520 # a different project and that is not systemwide shared
1518 1521 Issue.joins(:project, :fixed_version).
1519 1522 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1520 1523 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1521 1524 " AND #{Version.table_name}.sharing <> 'system'").
1522 1525 where(conditions).each do |issue|
1523 1526 next if issue.project.nil? || issue.fixed_version.nil?
1524 1527 unless issue.project.shared_versions.include?(issue.fixed_version)
1525 1528 issue.init_journal(User.current)
1526 1529 issue.fixed_version = nil
1527 1530 issue.save
1528 1531 end
1529 1532 end
1530 1533 end
1531 1534
1532 1535 # Callback on file attachment
1533 1536 def attachment_added(attachment)
1534 1537 if current_journal && !attachment.new_record?
1535 1538 current_journal.journalize_attachment(attachment, :added)
1536 1539 end
1537 1540 end
1538 1541
1539 1542 # Callback on attachment deletion
1540 1543 def attachment_removed(attachment)
1541 1544 if current_journal && !attachment.new_record?
1542 1545 current_journal.journalize_attachment(attachment, :removed)
1543 1546 current_journal.save
1544 1547 end
1545 1548 end
1546 1549
1547 1550 # Called after a relation is added
1548 1551 def relation_added(relation)
1549 1552 if current_journal
1550 1553 current_journal.journalize_relation(relation, :added)
1551 1554 current_journal.save
1552 1555 end
1553 1556 end
1554 1557
1555 1558 # Called after a relation is removed
1556 1559 def relation_removed(relation)
1557 1560 if current_journal
1558 1561 current_journal.journalize_relation(relation, :removed)
1559 1562 current_journal.save
1560 1563 end
1561 1564 end
1562 1565
1563 1566 # Default assignment based on category
1564 1567 def default_assign
1565 1568 if assigned_to.nil? && category && category.assigned_to
1566 1569 self.assigned_to = category.assigned_to
1567 1570 end
1568 1571 end
1569 1572
1570 1573 # Updates start/due dates of following issues
1571 1574 def reschedule_following_issues
1572 1575 if start_date_changed? || due_date_changed?
1573 1576 relations_from.each do |relation|
1574 1577 relation.set_issue_to_dates
1575 1578 end
1576 1579 end
1577 1580 end
1578 1581
1579 1582 # Closes duplicates if the issue is being closed
1580 1583 def close_duplicates
1581 1584 if closing?
1582 1585 duplicates.each do |duplicate|
1583 1586 # Reload is needed in case the duplicate was updated by a previous duplicate
1584 1587 duplicate.reload
1585 1588 # Don't re-close it if it's already closed
1586 1589 next if duplicate.closed?
1587 1590 # Same user and notes
1588 1591 if @current_journal
1589 1592 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1590 1593 end
1591 1594 duplicate.update_attribute :status, self.status
1592 1595 end
1593 1596 end
1594 1597 end
1595 1598
1596 1599 # Make sure updated_on is updated when adding a note and set updated_on now
1597 1600 # so we can set closed_on with the same value on closing
1598 1601 def force_updated_on_change
1599 1602 if @current_journal || changed?
1600 1603 self.updated_on = current_time_from_proper_timezone
1601 1604 if new_record?
1602 1605 self.created_on = updated_on
1603 1606 end
1604 1607 end
1605 1608 end
1606 1609
1607 1610 # Callback for setting closed_on when the issue is closed.
1608 1611 # The closed_on attribute stores the time of the last closing
1609 1612 # and is preserved when the issue is reopened.
1610 1613 def update_closed_on
1611 1614 if closing?
1612 1615 self.closed_on = updated_on
1613 1616 end
1614 1617 end
1615 1618
1616 1619 # Saves the changes in a Journal
1617 1620 # Called after_save
1618 1621 def create_journal
1619 1622 if current_journal
1620 1623 current_journal.save
1621 1624 end
1622 1625 end
1623 1626
1624 1627 def send_notification
1625 1628 if Setting.notified_events.include?('issue_added')
1626 1629 Mailer.deliver_issue_add(self)
1627 1630 end
1628 1631 end
1629 1632
1630 1633 # Stores the previous assignee so we can still have access
1631 1634 # to it during after_save callbacks (assigned_to_id_was is reset)
1632 1635 def set_assigned_to_was
1633 1636 @previous_assigned_to_id = assigned_to_id_was
1634 1637 end
1635 1638
1636 1639 # Clears the previous assignee at the end of after_save callbacks
1637 1640 def clear_assigned_to_was
1638 1641 @assigned_to_was = nil
1639 1642 @previous_assigned_to_id = nil
1640 1643 end
1641 1644
1642 1645 def clear_disabled_fields
1643 1646 if tracker
1644 1647 tracker.disabled_core_fields.each do |attribute|
1645 1648 send "#{attribute}=", nil
1646 1649 end
1647 1650 self.done_ratio ||= 0
1648 1651 end
1649 1652 end
1650 1653 end
@@ -1,123 +1,113
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id"
21 21 has_many :workflow_transitions_as_new_status, :class_name => 'WorkflowTransition', :foreign_key => "new_status_id"
22 22 acts_as_list
23 23
24 24 after_update :handle_is_closed_change
25 25 before_destroy :delete_workflow_rules
26 26
27 27 validates_presence_of :name
28 28 validates_uniqueness_of :name
29 29 validates_length_of :name, :maximum => 30
30 30 validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
31 31 attr_protected :id
32 32
33 33 scope :sorted, lambda { order(:position) }
34 34 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
35 35
36 36 # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
37 37 def self.update_issue_done_ratios
38 38 if Issue.use_status_for_done_ratio?
39 39 IssueStatus.where("default_done_ratio >= 0").each do |status|
40 40 Issue.where({:status_id => status.id}).update_all({:done_ratio => status.default_done_ratio})
41 41 end
42 42 end
43 43
44 44 return Issue.use_status_for_done_ratio?
45 45 end
46 46
47 47 # Returns an array of all statuses the given role can switch to
48 # Uses association cache when called more than one time
49 48 def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
50 if roles && tracker
51 role_ids = roles.collect(&:id)
52 transitions = workflows.select do |w|
53 role_ids.include?(w.role_id) &&
54 w.tracker_id == tracker.id &&
55 ((!w.author && !w.assignee) || (author && w.author) || (assignee && w.assignee))
56 end
57 transitions.map(&:new_status).compact.sort
58 else
59 []
60 end
49 self.class.new_statuses_allowed(self, roles, tracker, author, assignee)
61 50 end
51 alias :find_new_statuses_allowed_to :new_statuses_allowed_to
62 52
63 # Same thing as above but uses a database query
64 # More efficient than the previous method if called just once
65 def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
53 def self.new_statuses_allowed(status, roles, tracker, author=false, assignee=false)
66 54 if roles.present? && tracker
55 status_id = status.try(:id) || 0
56
67 57 scope = IssueStatus.
68 58 joins(:workflow_transitions_as_new_status).
69 where(:workflows => {:old_status_id => id, :role_id => roles.map(&:id), :tracker_id => tracker.id})
59 where(:workflows => {:old_status_id => status_id, :role_id => roles.map(&:id), :tracker_id => tracker.id})
70 60
71 61 unless author && assignee
72 62 if author || assignee
73 63 scope = scope.where("author = ? OR assignee = ?", author, assignee)
74 64 else
75 65 scope = scope.where("author = ? AND assignee = ?", false, false)
76 66 end
77 67 end
78 68
79 69 scope.uniq.to_a.sort
80 70 else
81 71 []
82 72 end
83 73 end
84 74
85 75 def <=>(status)
86 76 position <=> status.position
87 77 end
88 78
89 79 def to_s; name end
90 80
91 81 private
92 82
93 83 # Updates issues closed_on attribute when an existing status is set as closed.
94 84 def handle_is_closed_change
95 85 if is_closed_changed? && is_closed == true
96 86 # First we update issues that have a journal for when the current status was set,
97 87 # a subselect is used to update all issues with a single query
98 88 subselect = "SELECT MAX(j.created_on) FROM #{Journal.table_name} j" +
99 89 " JOIN #{JournalDetail.table_name} d ON d.journal_id = j.id" +
100 90 " WHERE j.journalized_type = 'Issue' AND j.journalized_id = #{Issue.table_name}.id" +
101 91 " AND d.property = 'attr' AND d.prop_key = 'status_id' AND d.value = :status_id"
102 92 Issue.where(:status_id => id, :closed_on => nil).
103 93 update_all(["closed_on = (#{subselect})", {:status_id => id.to_s}])
104 94
105 95 # Then we update issues that don't have a journal which means the
106 96 # current status was set on creation
107 97 Issue.where(:status_id => id, :closed_on => nil).update_all("closed_on = created_on")
108 98 end
109 99 end
110 100
111 101 def check_integrity
112 102 if Issue.where(:status_id => id).any?
113 103 raise "This status is used by some issues"
114 104 elsif Tracker.where(:default_status_id => id).any?
115 105 raise "This status is used as the default status by some trackers"
116 106 end
117 107 end
118 108
119 109 # Deletes associated workflows
120 110 def delete_workflow_rules
121 111 WorkflowRule.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
122 112 end
123 113 end
@@ -1,68 +1,69
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 WorkflowPermission < WorkflowRule
19 19 validates_inclusion_of :rule, :in => %w(readonly required)
20 validates_presence_of :old_status
20 21 validate :validate_field_name
21 22
22 23 # Returns the workflow permissions for the given trackers and roles
23 24 # grouped by status_id
24 25 #
25 26 # Example:
26 27 # WorkflowPermission.rules_by_status_id trackers, roles
27 28 # # => {1 => {'start_date' => 'required', 'due_date' => 'readonly'}}
28 29 def self.rules_by_status_id(trackers, roles)
29 30 WorkflowPermission.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id)).inject({}) do |h, w|
30 31 h[w.old_status_id] ||= {}
31 32 h[w.old_status_id][w.field_name] ||= []
32 33 h[w.old_status_id][w.field_name] << w.rule
33 34 h
34 35 end
35 36 end
36 37
37 38 # Replaces the workflow permissions for the given trackers and roles
38 39 #
39 40 # Example:
40 41 # WorkflowPermission.replace_permissions trackers, roles, {'1' => {'start_date' => 'required', 'due_date' => 'readonly'}}
41 42 def self.replace_permissions(trackers, roles, permissions)
42 43 trackers = Array.wrap trackers
43 44 roles = Array.wrap roles
44 45
45 46 transaction do
46 47 permissions.each { |status_id, rule_by_field|
47 48 rule_by_field.each { |field, rule|
48 49 destroy_all(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :old_status_id => status_id, :field_name => field)
49 50 if rule.present?
50 51 trackers.each do |tracker|
51 52 roles.each do |role|
52 53 WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule)
53 54 end
54 55 end
55 56 end
56 57 }
57 58 }
58 59 end
59 60 end
60 61
61 62 protected
62 63
63 64 def validate_field_name
64 65 unless Tracker::CORE_FIELDS_ALL.include?(field_name) || field_name.to_s.match(/^\d+$/)
65 66 errors.add :field_name, :invalid
66 67 end
67 68 end
68 69 end
@@ -1,74 +1,74
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 WorkflowRule < ActiveRecord::Base
19 19 self.table_name = "#{table_name_prefix}workflows#{table_name_suffix}"
20 20
21 21 belongs_to :role
22 22 belongs_to :tracker
23 23 belongs_to :old_status, :class_name => 'IssueStatus'
24 24 belongs_to :new_status, :class_name => 'IssueStatus'
25 25
26 validates_presence_of :role, :tracker, :old_status
26 validates_presence_of :role, :tracker
27 27 attr_protected :id
28 28
29 29 # Copies workflows from source to targets
30 30 def self.copy(source_tracker, source_role, target_trackers, target_roles)
31 31 unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role)
32 32 raise ArgumentError.new("source_tracker or source_role must be specified")
33 33 end
34 34
35 35 target_trackers = [target_trackers].flatten.compact
36 36 target_roles = [target_roles].flatten.compact
37 37
38 38 target_trackers = Tracker.sorted.to_a if target_trackers.empty?
39 39 target_roles = Role.all.select(&:consider_workflow?) if target_roles.empty?
40 40
41 41 target_trackers.each do |target_tracker|
42 42 target_roles.each do |target_role|
43 43 copy_one(source_tracker || target_tracker,
44 44 source_role || target_role,
45 45 target_tracker,
46 46 target_role)
47 47 end
48 48 end
49 49 end
50 50
51 51 # Copies a single set of workflows from source to target
52 52 def self.copy_one(source_tracker, source_role, target_tracker, target_role)
53 53 unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? &&
54 54 source_role.is_a?(Role) && !source_role.new_record? &&
55 55 target_tracker.is_a?(Tracker) && !target_tracker.new_record? &&
56 56 target_role.is_a?(Role) && !target_role.new_record?
57 57
58 58 raise ArgumentError.new("arguments can not be nil or unsaved objects")
59 59 end
60 60
61 61 if source_tracker == target_tracker && source_role == target_role
62 62 false
63 63 else
64 64 transaction do
65 65 delete_all :tracker_id => target_tracker.id, :role_id => target_role.id
66 66 connection.insert "INSERT INTO #{WorkflowRule.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type)" +
67 67 " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type" +
68 68 " FROM #{WorkflowRule.table_name}" +
69 69 " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}"
70 70 end
71 71 true
72 72 end
73 73 end
74 74 end
@@ -1,40 +1,41
1 1 <table class="list workflows transitions transitions-<%= name %>">
2 2 <thead>
3 3 <tr>
4 4 <th>
5 5 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input')",
6 6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
7 7 <%=l(:label_current_status)%>
8 8 </th>
9 9 <th colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
10 10 </tr>
11 11 <tr>
12 12 <td></td>
13 13 <% for new_status in @statuses %>
14 14 <td style="width:<%= 75 / @statuses.size %>%;">
15 15 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')",
16 16 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
17 17 <%= new_status.name %>
18 18 </td>
19 19 <% end %>
20 20 </tr>
21 21 </thead>
22 22 <tbody>
23 <% for old_status in @statuses %>
23 <% for old_status in [nil] + @statuses %>
24 <% next if old_status.nil? && name != 'always' %>
24 25 <tr class="<%= cycle("odd", "even") %>">
25 26 <td class="name">
26 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.id}')",
27 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.try(:id) || 0}')",
27 28 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
28 29
29 <%= old_status.name %>
30 <%= old_status ? old_status.name : content_tag('em', l(:label_issue_new)) %>
30 31 </td>
31 32 <% for new_status in @statuses -%>
32 <% checked = workflows.detect {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id} %>
33 <% checked = workflows.detect {|w| w.old_status == old_status && w.new_status == new_status} %>
33 34 <td class="<%= checked ? 'enabled' : '' %>">
34 35 <%= transition_tag workflows, old_status, new_status, name %>
35 36 </td>
36 37 <% end -%>
37 38 </tr>
38 39 <% end %>
39 40 </tbody>
40 41 </table>
@@ -1,1884 +1,1926
1 1 ---
2 2 WorkflowTransitions_189:
3 3 new_status_id: 5
4 4 role_id: 1
5 5 old_status_id: 2
6 6 id: 189
7 7 tracker_id: 3
8 8 type: WorkflowTransition
9 9 WorkflowTransitions_001:
10 10 new_status_id: 2
11 11 role_id: 1
12 12 old_status_id: 1
13 13 id: 1
14 14 tracker_id: 1
15 15 type: WorkflowTransition
16 16 WorkflowTransitions_002:
17 17 new_status_id: 3
18 18 role_id: 1
19 19 old_status_id: 1
20 20 id: 2
21 21 tracker_id: 1
22 22 type: WorkflowTransition
23 23 WorkflowTransitions_003:
24 24 new_status_id: 4
25 25 role_id: 1
26 26 old_status_id: 1
27 27 id: 3
28 28 tracker_id: 1
29 29 type: WorkflowTransition
30 30 WorkflowTransitions_110:
31 31 new_status_id: 6
32 32 role_id: 1
33 33 old_status_id: 4
34 34 id: 110
35 35 tracker_id: 2
36 36 type: WorkflowTransition
37 37 WorkflowTransitions_004:
38 38 new_status_id: 5
39 39 role_id: 1
40 40 old_status_id: 1
41 41 id: 4
42 42 tracker_id: 1
43 43 type: WorkflowTransition
44 44 WorkflowTransitions_030:
45 45 new_status_id: 5
46 46 role_id: 1
47 47 old_status_id: 6
48 48 id: 30
49 49 tracker_id: 1
50 50 type: WorkflowTransition
51 51 WorkflowTransitions_111:
52 52 new_status_id: 1
53 53 role_id: 1
54 54 old_status_id: 5
55 55 id: 111
56 56 tracker_id: 2
57 57 type: WorkflowTransition
58 58 WorkflowTransitions_005:
59 59 new_status_id: 6
60 60 role_id: 1
61 61 old_status_id: 1
62 62 id: 5
63 63 tracker_id: 1
64 64 type: WorkflowTransition
65 65 WorkflowTransitions_031:
66 66 new_status_id: 2
67 67 role_id: 2
68 68 old_status_id: 1
69 69 id: 31
70 70 tracker_id: 1
71 71 type: WorkflowTransition
72 72 WorkflowTransitions_112:
73 73 new_status_id: 2
74 74 role_id: 1
75 75 old_status_id: 5
76 76 id: 112
77 77 tracker_id: 2
78 78 type: WorkflowTransition
79 79 WorkflowTransitions_006:
80 80 new_status_id: 1
81 81 role_id: 1
82 82 old_status_id: 2
83 83 id: 6
84 84 tracker_id: 1
85 85 type: WorkflowTransition
86 86 WorkflowTransitions_032:
87 87 new_status_id: 3
88 88 role_id: 2
89 89 old_status_id: 1
90 90 id: 32
91 91 tracker_id: 1
92 92 type: WorkflowTransition
93 93 WorkflowTransitions_113:
94 94 new_status_id: 3
95 95 role_id: 1
96 96 old_status_id: 5
97 97 id: 113
98 98 tracker_id: 2
99 99 type: WorkflowTransition
100 100 WorkflowTransitions_220:
101 101 new_status_id: 6
102 102 role_id: 2
103 103 old_status_id: 2
104 104 id: 220
105 105 tracker_id: 3
106 106 type: WorkflowTransition
107 107 WorkflowTransitions_007:
108 108 new_status_id: 3
109 109 role_id: 1
110 110 old_status_id: 2
111 111 id: 7
112 112 tracker_id: 1
113 113 type: WorkflowTransition
114 114 WorkflowTransitions_033:
115 115 new_status_id: 4
116 116 role_id: 2
117 117 old_status_id: 1
118 118 id: 33
119 119 tracker_id: 1
120 120 type: WorkflowTransition
121 121 WorkflowTransitions_060:
122 122 new_status_id: 5
123 123 role_id: 2
124 124 old_status_id: 6
125 125 id: 60
126 126 tracker_id: 1
127 127 type: WorkflowTransition
128 128 WorkflowTransitions_114:
129 129 new_status_id: 4
130 130 role_id: 1
131 131 old_status_id: 5
132 132 id: 114
133 133 tracker_id: 2
134 134 type: WorkflowTransition
135 135 WorkflowTransitions_140:
136 136 new_status_id: 6
137 137 role_id: 2
138 138 old_status_id: 4
139 139 id: 140
140 140 tracker_id: 2
141 141 type: WorkflowTransition
142 142 WorkflowTransitions_221:
143 143 new_status_id: 1
144 144 role_id: 2
145 145 old_status_id: 3
146 146 id: 221
147 147 tracker_id: 3
148 148 type: WorkflowTransition
149 149 WorkflowTransitions_008:
150 150 new_status_id: 4
151 151 role_id: 1
152 152 old_status_id: 2
153 153 id: 8
154 154 tracker_id: 1
155 155 type: WorkflowTransition
156 156 WorkflowTransitions_034:
157 157 new_status_id: 5
158 158 role_id: 2
159 159 old_status_id: 1
160 160 id: 34
161 161 tracker_id: 1
162 162 type: WorkflowTransition
163 163 WorkflowTransitions_115:
164 164 new_status_id: 6
165 165 role_id: 1
166 166 old_status_id: 5
167 167 id: 115
168 168 tracker_id: 2
169 169 type: WorkflowTransition
170 170 WorkflowTransitions_141:
171 171 new_status_id: 1
172 172 role_id: 2
173 173 old_status_id: 5
174 174 id: 141
175 175 tracker_id: 2
176 176 type: WorkflowTransition
177 177 WorkflowTransitions_222:
178 178 new_status_id: 2
179 179 role_id: 2
180 180 old_status_id: 3
181 181 id: 222
182 182 tracker_id: 3
183 183 type: WorkflowTransition
184 184 WorkflowTransitions_223:
185 185 new_status_id: 4
186 186 role_id: 2
187 187 old_status_id: 3
188 188 id: 223
189 189 tracker_id: 3
190 190 type: WorkflowTransition
191 191 WorkflowTransitions_009:
192 192 new_status_id: 5
193 193 role_id: 1
194 194 old_status_id: 2
195 195 id: 9
196 196 tracker_id: 1
197 197 type: WorkflowTransition
198 198 WorkflowTransitions_035:
199 199 new_status_id: 6
200 200 role_id: 2
201 201 old_status_id: 1
202 202 id: 35
203 203 tracker_id: 1
204 204 type: WorkflowTransition
205 205 WorkflowTransitions_061:
206 206 new_status_id: 2
207 207 role_id: 3
208 208 old_status_id: 1
209 209 id: 61
210 210 tracker_id: 1
211 211 type: WorkflowTransition
212 212 WorkflowTransitions_116:
213 213 new_status_id: 1
214 214 role_id: 1
215 215 old_status_id: 6
216 216 id: 116
217 217 tracker_id: 2
218 218 type: WorkflowTransition
219 219 WorkflowTransitions_142:
220 220 new_status_id: 2
221 221 role_id: 2
222 222 old_status_id: 5
223 223 id: 142
224 224 tracker_id: 2
225 225 type: WorkflowTransition
226 226 WorkflowTransitions_250:
227 227 new_status_id: 6
228 228 role_id: 3
229 229 old_status_id: 2
230 230 id: 250
231 231 tracker_id: 3
232 232 type: WorkflowTransition
233 233 WorkflowTransitions_224:
234 234 new_status_id: 5
235 235 role_id: 2
236 236 old_status_id: 3
237 237 id: 224
238 238 tracker_id: 3
239 239 type: WorkflowTransition
240 240 WorkflowTransitions_036:
241 241 new_status_id: 1
242 242 role_id: 2
243 243 old_status_id: 2
244 244 id: 36
245 245 tracker_id: 1
246 246 type: WorkflowTransition
247 247 WorkflowTransitions_062:
248 248 new_status_id: 3
249 249 role_id: 3
250 250 old_status_id: 1
251 251 id: 62
252 252 tracker_id: 1
253 253 type: WorkflowTransition
254 254 WorkflowTransitions_117:
255 255 new_status_id: 2
256 256 role_id: 1
257 257 old_status_id: 6
258 258 id: 117
259 259 tracker_id: 2
260 260 type: WorkflowTransition
261 261 WorkflowTransitions_143:
262 262 new_status_id: 3
263 263 role_id: 2
264 264 old_status_id: 5
265 265 id: 143
266 266 tracker_id: 2
267 267 type: WorkflowTransition
268 268 WorkflowTransitions_170:
269 269 new_status_id: 6
270 270 role_id: 3
271 271 old_status_id: 4
272 272 id: 170
273 273 tracker_id: 2
274 274 type: WorkflowTransition
275 275 WorkflowTransitions_251:
276 276 new_status_id: 1
277 277 role_id: 3
278 278 old_status_id: 3
279 279 id: 251
280 280 tracker_id: 3
281 281 type: WorkflowTransition
282 282 WorkflowTransitions_225:
283 283 new_status_id: 6
284 284 role_id: 2
285 285 old_status_id: 3
286 286 id: 225
287 287 tracker_id: 3
288 288 type: WorkflowTransition
289 289 WorkflowTransitions_063:
290 290 new_status_id: 4
291 291 role_id: 3
292 292 old_status_id: 1
293 293 id: 63
294 294 tracker_id: 1
295 295 type: WorkflowTransition
296 296 WorkflowTransitions_090:
297 297 new_status_id: 5
298 298 role_id: 3
299 299 old_status_id: 6
300 300 id: 90
301 301 tracker_id: 1
302 302 type: WorkflowTransition
303 303 WorkflowTransitions_118:
304 304 new_status_id: 3
305 305 role_id: 1
306 306 old_status_id: 6
307 307 id: 118
308 308 tracker_id: 2
309 309 type: WorkflowTransition
310 310 WorkflowTransitions_144:
311 311 new_status_id: 4
312 312 role_id: 2
313 313 old_status_id: 5
314 314 id: 144
315 315 tracker_id: 2
316 316 type: WorkflowTransition
317 317 WorkflowTransitions_252:
318 318 new_status_id: 2
319 319 role_id: 3
320 320 old_status_id: 3
321 321 id: 252
322 322 tracker_id: 3
323 323 type: WorkflowTransition
324 324 WorkflowTransitions_226:
325 325 new_status_id: 1
326 326 role_id: 2
327 327 old_status_id: 4
328 328 id: 226
329 329 tracker_id: 3
330 330 type: WorkflowTransition
331 331 WorkflowTransitions_038:
332 332 new_status_id: 4
333 333 role_id: 2
334 334 old_status_id: 2
335 335 id: 38
336 336 tracker_id: 1
337 337 type: WorkflowTransition
338 338 WorkflowTransitions_064:
339 339 new_status_id: 5
340 340 role_id: 3
341 341 old_status_id: 1
342 342 id: 64
343 343 tracker_id: 1
344 344 type: WorkflowTransition
345 345 WorkflowTransitions_091:
346 346 new_status_id: 2
347 347 role_id: 1
348 348 old_status_id: 1
349 349 id: 91
350 350 tracker_id: 2
351 351 type: WorkflowTransition
352 352 WorkflowTransitions_119:
353 353 new_status_id: 4
354 354 role_id: 1
355 355 old_status_id: 6
356 356 id: 119
357 357 tracker_id: 2
358 358 type: WorkflowTransition
359 359 WorkflowTransitions_145:
360 360 new_status_id: 6
361 361 role_id: 2
362 362 old_status_id: 5
363 363 id: 145
364 364 tracker_id: 2
365 365 type: WorkflowTransition
366 366 WorkflowTransitions_171:
367 367 new_status_id: 1
368 368 role_id: 3
369 369 old_status_id: 5
370 370 id: 171
371 371 tracker_id: 2
372 372 type: WorkflowTransition
373 373 WorkflowTransitions_253:
374 374 new_status_id: 4
375 375 role_id: 3
376 376 old_status_id: 3
377 377 id: 253
378 378 tracker_id: 3
379 379 type: WorkflowTransition
380 380 WorkflowTransitions_227:
381 381 new_status_id: 2
382 382 role_id: 2
383 383 old_status_id: 4
384 384 id: 227
385 385 tracker_id: 3
386 386 type: WorkflowTransition
387 387 WorkflowTransitions_039:
388 388 new_status_id: 5
389 389 role_id: 2
390 390 old_status_id: 2
391 391 id: 39
392 392 tracker_id: 1
393 393 type: WorkflowTransition
394 394 WorkflowTransitions_065:
395 395 new_status_id: 6
396 396 role_id: 3
397 397 old_status_id: 1
398 398 id: 65
399 399 tracker_id: 1
400 400 type: WorkflowTransition
401 401 WorkflowTransitions_092:
402 402 new_status_id: 3
403 403 role_id: 1
404 404 old_status_id: 1
405 405 id: 92
406 406 tracker_id: 2
407 407 type: WorkflowTransition
408 408 WorkflowTransitions_146:
409 409 new_status_id: 1
410 410 role_id: 2
411 411 old_status_id: 6
412 412 id: 146
413 413 tracker_id: 2
414 414 type: WorkflowTransition
415 415 WorkflowTransitions_172:
416 416 new_status_id: 2
417 417 role_id: 3
418 418 old_status_id: 5
419 419 id: 172
420 420 tracker_id: 2
421 421 type: WorkflowTransition
422 422 WorkflowTransitions_254:
423 423 new_status_id: 5
424 424 role_id: 3
425 425 old_status_id: 3
426 426 id: 254
427 427 tracker_id: 3
428 428 type: WorkflowTransition
429 429 WorkflowTransitions_228:
430 430 new_status_id: 3
431 431 role_id: 2
432 432 old_status_id: 4
433 433 id: 228
434 434 tracker_id: 3
435 435 type: WorkflowTransition
436 436 WorkflowTransitions_066:
437 437 new_status_id: 1
438 438 role_id: 3
439 439 old_status_id: 2
440 440 id: 66
441 441 tracker_id: 1
442 442 type: WorkflowTransition
443 443 WorkflowTransitions_093:
444 444 new_status_id: 4
445 445 role_id: 1
446 446 old_status_id: 1
447 447 id: 93
448 448 tracker_id: 2
449 449 type: WorkflowTransition
450 450 WorkflowTransitions_147:
451 451 new_status_id: 2
452 452 role_id: 2
453 453 old_status_id: 6
454 454 id: 147
455 455 tracker_id: 2
456 456 type: WorkflowTransition
457 457 WorkflowTransitions_173:
458 458 new_status_id: 3
459 459 role_id: 3
460 460 old_status_id: 5
461 461 id: 173
462 462 tracker_id: 2
463 463 type: WorkflowTransition
464 464 WorkflowTransitions_255:
465 465 new_status_id: 6
466 466 role_id: 3
467 467 old_status_id: 3
468 468 id: 255
469 469 tracker_id: 3
470 470 type: WorkflowTransition
471 471 WorkflowTransitions_229:
472 472 new_status_id: 5
473 473 role_id: 2
474 474 old_status_id: 4
475 475 id: 229
476 476 tracker_id: 3
477 477 type: WorkflowTransition
478 478 WorkflowTransitions_067:
479 479 new_status_id: 3
480 480 role_id: 3
481 481 old_status_id: 2
482 482 id: 67
483 483 tracker_id: 1
484 484 type: WorkflowTransition
485 485 WorkflowTransitions_148:
486 486 new_status_id: 3
487 487 role_id: 2
488 488 old_status_id: 6
489 489 id: 148
490 490 tracker_id: 2
491 491 type: WorkflowTransition
492 492 WorkflowTransitions_174:
493 493 new_status_id: 4
494 494 role_id: 3
495 495 old_status_id: 5
496 496 id: 174
497 497 tracker_id: 2
498 498 type: WorkflowTransition
499 499 WorkflowTransitions_256:
500 500 new_status_id: 1
501 501 role_id: 3
502 502 old_status_id: 4
503 503 id: 256
504 504 tracker_id: 3
505 505 type: WorkflowTransition
506 506 WorkflowTransitions_068:
507 507 new_status_id: 4
508 508 role_id: 3
509 509 old_status_id: 2
510 510 id: 68
511 511 tracker_id: 1
512 512 type: WorkflowTransition
513 513 WorkflowTransitions_094:
514 514 new_status_id: 5
515 515 role_id: 1
516 516 old_status_id: 1
517 517 id: 94
518 518 tracker_id: 2
519 519 type: WorkflowTransition
520 520 WorkflowTransitions_149:
521 521 new_status_id: 4
522 522 role_id: 2
523 523 old_status_id: 6
524 524 id: 149
525 525 tracker_id: 2
526 526 type: WorkflowTransition
527 527 WorkflowTransitions_175:
528 528 new_status_id: 6
529 529 role_id: 3
530 530 old_status_id: 5
531 531 id: 175
532 532 tracker_id: 2
533 533 type: WorkflowTransition
534 534 WorkflowTransitions_257:
535 535 new_status_id: 2
536 536 role_id: 3
537 537 old_status_id: 4
538 538 id: 257
539 539 tracker_id: 3
540 540 type: WorkflowTransition
541 541 WorkflowTransitions_069:
542 542 new_status_id: 5
543 543 role_id: 3
544 544 old_status_id: 2
545 545 id: 69
546 546 tracker_id: 1
547 547 type: WorkflowTransition
548 548 WorkflowTransitions_095:
549 549 new_status_id: 6
550 550 role_id: 1
551 551 old_status_id: 1
552 552 id: 95
553 553 tracker_id: 2
554 554 type: WorkflowTransition
555 555 WorkflowTransitions_176:
556 556 new_status_id: 1
557 557 role_id: 3
558 558 old_status_id: 6
559 559 id: 176
560 560 tracker_id: 2
561 561 type: WorkflowTransition
562 562 WorkflowTransitions_258:
563 563 new_status_id: 3
564 564 role_id: 3
565 565 old_status_id: 4
566 566 id: 258
567 567 tracker_id: 3
568 568 type: WorkflowTransition
569 569 WorkflowTransitions_096:
570 570 new_status_id: 1
571 571 role_id: 1
572 572 old_status_id: 2
573 573 id: 96
574 574 tracker_id: 2
575 575 type: WorkflowTransition
576 576 WorkflowTransitions_177:
577 577 new_status_id: 2
578 578 role_id: 3
579 579 old_status_id: 6
580 580 id: 177
581 581 tracker_id: 2
582 582 type: WorkflowTransition
583 583 WorkflowTransitions_259:
584 584 new_status_id: 5
585 585 role_id: 3
586 586 old_status_id: 4
587 587 id: 259
588 588 tracker_id: 3
589 589 type: WorkflowTransition
590 590 WorkflowTransitions_097:
591 591 new_status_id: 3
592 592 role_id: 1
593 593 old_status_id: 2
594 594 id: 97
595 595 tracker_id: 2
596 596 type: WorkflowTransition
597 597 WorkflowTransitions_178:
598 598 new_status_id: 3
599 599 role_id: 3
600 600 old_status_id: 6
601 601 id: 178
602 602 tracker_id: 2
603 603 type: WorkflowTransition
604 604 WorkflowTransitions_098:
605 605 new_status_id: 4
606 606 role_id: 1
607 607 old_status_id: 2
608 608 id: 98
609 609 tracker_id: 2
610 610 type: WorkflowTransition
611 611 WorkflowTransitions_179:
612 612 new_status_id: 4
613 613 role_id: 3
614 614 old_status_id: 6
615 615 id: 179
616 616 tracker_id: 2
617 617 type: WorkflowTransition
618 618 WorkflowTransitions_099:
619 619 new_status_id: 5
620 620 role_id: 1
621 621 old_status_id: 2
622 622 id: 99
623 623 tracker_id: 2
624 624 type: WorkflowTransition
625 625 WorkflowTransitions_100:
626 626 new_status_id: 6
627 627 role_id: 1
628 628 old_status_id: 2
629 629 id: 100
630 630 tracker_id: 2
631 631 type: WorkflowTransition
632 632 WorkflowTransitions_020:
633 633 new_status_id: 6
634 634 role_id: 1
635 635 old_status_id: 4
636 636 id: 20
637 637 tracker_id: 1
638 638 type: WorkflowTransition
639 639 WorkflowTransitions_101:
640 640 new_status_id: 1
641 641 role_id: 1
642 642 old_status_id: 3
643 643 id: 101
644 644 tracker_id: 2
645 645 type: WorkflowTransition
646 646 WorkflowTransitions_021:
647 647 new_status_id: 1
648 648 role_id: 1
649 649 old_status_id: 5
650 650 id: 21
651 651 tracker_id: 1
652 652 type: WorkflowTransition
653 653 WorkflowTransitions_102:
654 654 new_status_id: 2
655 655 role_id: 1
656 656 old_status_id: 3
657 657 id: 102
658 658 tracker_id: 2
659 659 type: WorkflowTransition
660 660 WorkflowTransitions_210:
661 661 new_status_id: 5
662 662 role_id: 1
663 663 old_status_id: 6
664 664 id: 210
665 665 tracker_id: 3
666 666 type: WorkflowTransition
667 667 WorkflowTransitions_022:
668 668 new_status_id: 2
669 669 role_id: 1
670 670 old_status_id: 5
671 671 id: 22
672 672 tracker_id: 1
673 673 type: WorkflowTransition
674 674 WorkflowTransitions_103:
675 675 new_status_id: 4
676 676 role_id: 1
677 677 old_status_id: 3
678 678 id: 103
679 679 tracker_id: 2
680 680 type: WorkflowTransition
681 681 WorkflowTransitions_023:
682 682 new_status_id: 3
683 683 role_id: 1
684 684 old_status_id: 5
685 685 id: 23
686 686 tracker_id: 1
687 687 type: WorkflowTransition
688 688 WorkflowTransitions_104:
689 689 new_status_id: 5
690 690 role_id: 1
691 691 old_status_id: 3
692 692 id: 104
693 693 tracker_id: 2
694 694 type: WorkflowTransition
695 695 WorkflowTransitions_130:
696 696 new_status_id: 6
697 697 role_id: 2
698 698 old_status_id: 2
699 699 id: 130
700 700 tracker_id: 2
701 701 type: WorkflowTransition
702 702 WorkflowTransitions_211:
703 703 new_status_id: 2
704 704 role_id: 2
705 705 old_status_id: 1
706 706 id: 211
707 707 tracker_id: 3
708 708 type: WorkflowTransition
709 709 WorkflowTransitions_024:
710 710 new_status_id: 4
711 711 role_id: 1
712 712 old_status_id: 5
713 713 id: 24
714 714 tracker_id: 1
715 715 type: WorkflowTransition
716 716 WorkflowTransitions_050:
717 717 new_status_id: 6
718 718 role_id: 2
719 719 old_status_id: 4
720 720 id: 50
721 721 tracker_id: 1
722 722 type: WorkflowTransition
723 723 WorkflowTransitions_105:
724 724 new_status_id: 6
725 725 role_id: 1
726 726 old_status_id: 3
727 727 id: 105
728 728 tracker_id: 2
729 729 type: WorkflowTransition
730 730 WorkflowTransitions_131:
731 731 new_status_id: 1
732 732 role_id: 2
733 733 old_status_id: 3
734 734 id: 131
735 735 tracker_id: 2
736 736 type: WorkflowTransition
737 737 WorkflowTransitions_212:
738 738 new_status_id: 3
739 739 role_id: 2
740 740 old_status_id: 1
741 741 id: 212
742 742 tracker_id: 3
743 743 type: WorkflowTransition
744 744 WorkflowTransitions_025:
745 745 new_status_id: 6
746 746 role_id: 1
747 747 old_status_id: 5
748 748 id: 25
749 749 tracker_id: 1
750 750 type: WorkflowTransition
751 751 WorkflowTransitions_051:
752 752 new_status_id: 1
753 753 role_id: 2
754 754 old_status_id: 5
755 755 id: 51
756 756 tracker_id: 1
757 757 type: WorkflowTransition
758 758 WorkflowTransitions_106:
759 759 new_status_id: 1
760 760 role_id: 1
761 761 old_status_id: 4
762 762 id: 106
763 763 tracker_id: 2
764 764 type: WorkflowTransition
765 765 WorkflowTransitions_132:
766 766 new_status_id: 2
767 767 role_id: 2
768 768 old_status_id: 3
769 769 id: 132
770 770 tracker_id: 2
771 771 type: WorkflowTransition
772 772 WorkflowTransitions_213:
773 773 new_status_id: 4
774 774 role_id: 2
775 775 old_status_id: 1
776 776 id: 213
777 777 tracker_id: 3
778 778 type: WorkflowTransition
779 779 WorkflowTransitions_240:
780 780 new_status_id: 5
781 781 role_id: 2
782 782 old_status_id: 6
783 783 id: 240
784 784 tracker_id: 3
785 785 type: WorkflowTransition
786 786 WorkflowTransitions_026:
787 787 new_status_id: 1
788 788 role_id: 1
789 789 old_status_id: 6
790 790 id: 26
791 791 tracker_id: 1
792 792 type: WorkflowTransition
793 793 WorkflowTransitions_052:
794 794 new_status_id: 2
795 795 role_id: 2
796 796 old_status_id: 5
797 797 id: 52
798 798 tracker_id: 1
799 799 type: WorkflowTransition
800 800 WorkflowTransitions_107:
801 801 new_status_id: 2
802 802 role_id: 1
803 803 old_status_id: 4
804 804 id: 107
805 805 tracker_id: 2
806 806 type: WorkflowTransition
807 807 WorkflowTransitions_133:
808 808 new_status_id: 4
809 809 role_id: 2
810 810 old_status_id: 3
811 811 id: 133
812 812 tracker_id: 2
813 813 type: WorkflowTransition
814 814 WorkflowTransitions_214:
815 815 new_status_id: 5
816 816 role_id: 2
817 817 old_status_id: 1
818 818 id: 214
819 819 tracker_id: 3
820 820 type: WorkflowTransition
821 821 WorkflowTransitions_241:
822 822 new_status_id: 2
823 823 role_id: 3
824 824 old_status_id: 1
825 825 id: 241
826 826 tracker_id: 3
827 827 type: WorkflowTransition
828 828 WorkflowTransitions_027:
829 829 new_status_id: 2
830 830 role_id: 1
831 831 old_status_id: 6
832 832 id: 27
833 833 tracker_id: 1
834 834 type: WorkflowTransition
835 835 WorkflowTransitions_053:
836 836 new_status_id: 3
837 837 role_id: 2
838 838 old_status_id: 5
839 839 id: 53
840 840 tracker_id: 1
841 841 type: WorkflowTransition
842 842 WorkflowTransitions_080:
843 843 new_status_id: 6
844 844 role_id: 3
845 845 old_status_id: 4
846 846 id: 80
847 847 tracker_id: 1
848 848 type: WorkflowTransition
849 849 WorkflowTransitions_108:
850 850 new_status_id: 3
851 851 role_id: 1
852 852 old_status_id: 4
853 853 id: 108
854 854 tracker_id: 2
855 855 type: WorkflowTransition
856 856 WorkflowTransitions_134:
857 857 new_status_id: 5
858 858 role_id: 2
859 859 old_status_id: 3
860 860 id: 134
861 861 tracker_id: 2
862 862 type: WorkflowTransition
863 863 WorkflowTransitions_160:
864 864 new_status_id: 6
865 865 role_id: 3
866 866 old_status_id: 2
867 867 id: 160
868 868 tracker_id: 2
869 869 type: WorkflowTransition
870 870 WorkflowTransitions_215:
871 871 new_status_id: 6
872 872 role_id: 2
873 873 old_status_id: 1
874 874 id: 215
875 875 tracker_id: 3
876 876 type: WorkflowTransition
877 877 WorkflowTransitions_242:
878 878 new_status_id: 3
879 879 role_id: 3
880 880 old_status_id: 1
881 881 id: 242
882 882 tracker_id: 3
883 883 type: WorkflowTransition
884 884 WorkflowTransitions_028:
885 885 new_status_id: 3
886 886 role_id: 1
887 887 old_status_id: 6
888 888 id: 28
889 889 tracker_id: 1
890 890 type: WorkflowTransition
891 891 WorkflowTransitions_054:
892 892 new_status_id: 4
893 893 role_id: 2
894 894 old_status_id: 5
895 895 id: 54
896 896 tracker_id: 1
897 897 type: WorkflowTransition
898 898 WorkflowTransitions_081:
899 899 new_status_id: 1
900 900 role_id: 3
901 901 old_status_id: 5
902 902 id: 81
903 903 tracker_id: 1
904 904 type: WorkflowTransition
905 905 WorkflowTransitions_109:
906 906 new_status_id: 5
907 907 role_id: 1
908 908 old_status_id: 4
909 909 id: 109
910 910 tracker_id: 2
911 911 type: WorkflowTransition
912 912 WorkflowTransitions_135:
913 913 new_status_id: 6
914 914 role_id: 2
915 915 old_status_id: 3
916 916 id: 135
917 917 tracker_id: 2
918 918 type: WorkflowTransition
919 919 WorkflowTransitions_161:
920 920 new_status_id: 1
921 921 role_id: 3
922 922 old_status_id: 3
923 923 id: 161
924 924 tracker_id: 2
925 925 type: WorkflowTransition
926 926 WorkflowTransitions_216:
927 927 new_status_id: 1
928 928 role_id: 2
929 929 old_status_id: 2
930 930 id: 216
931 931 tracker_id: 3
932 932 type: WorkflowTransition
933 933 WorkflowTransitions_243:
934 934 new_status_id: 4
935 935 role_id: 3
936 936 old_status_id: 1
937 937 id: 243
938 938 tracker_id: 3
939 939 type: WorkflowTransition
940 940 WorkflowTransitions_029:
941 941 new_status_id: 4
942 942 role_id: 1
943 943 old_status_id: 6
944 944 id: 29
945 945 tracker_id: 1
946 946 type: WorkflowTransition
947 947 WorkflowTransitions_055:
948 948 new_status_id: 6
949 949 role_id: 2
950 950 old_status_id: 5
951 951 id: 55
952 952 tracker_id: 1
953 953 type: WorkflowTransition
954 954 WorkflowTransitions_082:
955 955 new_status_id: 2
956 956 role_id: 3
957 957 old_status_id: 5
958 958 id: 82
959 959 tracker_id: 1
960 960 type: WorkflowTransition
961 961 WorkflowTransitions_136:
962 962 new_status_id: 1
963 963 role_id: 2
964 964 old_status_id: 4
965 965 id: 136
966 966 tracker_id: 2
967 967 type: WorkflowTransition
968 968 WorkflowTransitions_162:
969 969 new_status_id: 2
970 970 role_id: 3
971 971 old_status_id: 3
972 972 id: 162
973 973 tracker_id: 2
974 974 type: WorkflowTransition
975 975 WorkflowTransitions_217:
976 976 new_status_id: 3
977 977 role_id: 2
978 978 old_status_id: 2
979 979 id: 217
980 980 tracker_id: 3
981 981 type: WorkflowTransition
982 982 WorkflowTransitions_270:
983 983 new_status_id: 5
984 984 role_id: 3
985 985 old_status_id: 6
986 986 id: 270
987 987 tracker_id: 3
988 988 type: WorkflowTransition
989 989 WorkflowTransitions_244:
990 990 new_status_id: 5
991 991 role_id: 3
992 992 old_status_id: 1
993 993 id: 244
994 994 tracker_id: 3
995 995 type: WorkflowTransition
996 996 WorkflowTransitions_056:
997 997 new_status_id: 1
998 998 role_id: 2
999 999 old_status_id: 6
1000 1000 id: 56
1001 1001 tracker_id: 1
1002 1002 type: WorkflowTransition
1003 1003 WorkflowTransitions_137:
1004 1004 new_status_id: 2
1005 1005 role_id: 2
1006 1006 old_status_id: 4
1007 1007 id: 137
1008 1008 tracker_id: 2
1009 1009 type: WorkflowTransition
1010 1010 WorkflowTransitions_163:
1011 1011 new_status_id: 4
1012 1012 role_id: 3
1013 1013 old_status_id: 3
1014 1014 id: 163
1015 1015 tracker_id: 2
1016 1016 type: WorkflowTransition
1017 1017 WorkflowTransitions_190:
1018 1018 new_status_id: 6
1019 1019 role_id: 1
1020 1020 old_status_id: 2
1021 1021 id: 190
1022 1022 tracker_id: 3
1023 1023 type: WorkflowTransition
1024 1024 WorkflowTransitions_218:
1025 1025 new_status_id: 4
1026 1026 role_id: 2
1027 1027 old_status_id: 2
1028 1028 id: 218
1029 1029 tracker_id: 3
1030 1030 type: WorkflowTransition
1031 1031 WorkflowTransitions_245:
1032 1032 new_status_id: 6
1033 1033 role_id: 3
1034 1034 old_status_id: 1
1035 1035 id: 245
1036 1036 tracker_id: 3
1037 1037 type: WorkflowTransition
1038 1038 WorkflowTransitions_057:
1039 1039 new_status_id: 2
1040 1040 role_id: 2
1041 1041 old_status_id: 6
1042 1042 id: 57
1043 1043 tracker_id: 1
1044 1044 type: WorkflowTransition
1045 1045 WorkflowTransitions_083:
1046 1046 new_status_id: 3
1047 1047 role_id: 3
1048 1048 old_status_id: 5
1049 1049 id: 83
1050 1050 tracker_id: 1
1051 1051 type: WorkflowTransition
1052 1052 WorkflowTransitions_138:
1053 1053 new_status_id: 3
1054 1054 role_id: 2
1055 1055 old_status_id: 4
1056 1056 id: 138
1057 1057 tracker_id: 2
1058 1058 type: WorkflowTransition
1059 1059 WorkflowTransitions_164:
1060 1060 new_status_id: 5
1061 1061 role_id: 3
1062 1062 old_status_id: 3
1063 1063 id: 164
1064 1064 tracker_id: 2
1065 1065 type: WorkflowTransition
1066 1066 WorkflowTransitions_191:
1067 1067 new_status_id: 1
1068 1068 role_id: 1
1069 1069 old_status_id: 3
1070 1070 id: 191
1071 1071 tracker_id: 3
1072 1072 type: WorkflowTransition
1073 1073 WorkflowTransitions_219:
1074 1074 new_status_id: 5
1075 1075 role_id: 2
1076 1076 old_status_id: 2
1077 1077 id: 219
1078 1078 tracker_id: 3
1079 1079 type: WorkflowTransition
1080 1080 WorkflowTransitions_246:
1081 1081 new_status_id: 1
1082 1082 role_id: 3
1083 1083 old_status_id: 2
1084 1084 id: 246
1085 1085 tracker_id: 3
1086 1086 type: WorkflowTransition
1087 1087 WorkflowTransitions_058:
1088 1088 new_status_id: 3
1089 1089 role_id: 2
1090 1090 old_status_id: 6
1091 1091 id: 58
1092 1092 tracker_id: 1
1093 1093 type: WorkflowTransition
1094 1094 WorkflowTransitions_084:
1095 1095 new_status_id: 4
1096 1096 role_id: 3
1097 1097 old_status_id: 5
1098 1098 id: 84
1099 1099 tracker_id: 1
1100 1100 type: WorkflowTransition
1101 1101 WorkflowTransitions_139:
1102 1102 new_status_id: 5
1103 1103 role_id: 2
1104 1104 old_status_id: 4
1105 1105 id: 139
1106 1106 tracker_id: 2
1107 1107 type: WorkflowTransition
1108 1108 WorkflowTransitions_165:
1109 1109 new_status_id: 6
1110 1110 role_id: 3
1111 1111 old_status_id: 3
1112 1112 id: 165
1113 1113 tracker_id: 2
1114 1114 type: WorkflowTransition
1115 1115 WorkflowTransitions_192:
1116 1116 new_status_id: 2
1117 1117 role_id: 1
1118 1118 old_status_id: 3
1119 1119 id: 192
1120 1120 tracker_id: 3
1121 1121 type: WorkflowTransition
1122 1122 WorkflowTransitions_247:
1123 1123 new_status_id: 3
1124 1124 role_id: 3
1125 1125 old_status_id: 2
1126 1126 id: 247
1127 1127 tracker_id: 3
1128 1128 type: WorkflowTransition
1129 1129 WorkflowTransitions_059:
1130 1130 new_status_id: 4
1131 1131 role_id: 2
1132 1132 old_status_id: 6
1133 1133 id: 59
1134 1134 tracker_id: 1
1135 1135 type: WorkflowTransition
1136 1136 WorkflowTransitions_085:
1137 1137 new_status_id: 6
1138 1138 role_id: 3
1139 1139 old_status_id: 5
1140 1140 id: 85
1141 1141 tracker_id: 1
1142 1142 type: WorkflowTransition
1143 1143 WorkflowTransitions_166:
1144 1144 new_status_id: 1
1145 1145 role_id: 3
1146 1146 old_status_id: 4
1147 1147 id: 166
1148 1148 tracker_id: 2
1149 1149 type: WorkflowTransition
1150 1150 WorkflowTransitions_248:
1151 1151 new_status_id: 4
1152 1152 role_id: 3
1153 1153 old_status_id: 2
1154 1154 id: 248
1155 1155 tracker_id: 3
1156 1156 type: WorkflowTransition
1157 1157 WorkflowTransitions_086:
1158 1158 new_status_id: 1
1159 1159 role_id: 3
1160 1160 old_status_id: 6
1161 1161 id: 86
1162 1162 tracker_id: 1
1163 1163 type: WorkflowTransition
1164 1164 WorkflowTransitions_167:
1165 1165 new_status_id: 2
1166 1166 role_id: 3
1167 1167 old_status_id: 4
1168 1168 id: 167
1169 1169 tracker_id: 2
1170 1170 type: WorkflowTransition
1171 1171 WorkflowTransitions_193:
1172 1172 new_status_id: 4
1173 1173 role_id: 1
1174 1174 old_status_id: 3
1175 1175 id: 193
1176 1176 tracker_id: 3
1177 1177 type: WorkflowTransition
1178 1178 WorkflowTransitions_249:
1179 1179 new_status_id: 5
1180 1180 role_id: 3
1181 1181 old_status_id: 2
1182 1182 id: 249
1183 1183 tracker_id: 3
1184 1184 type: WorkflowTransition
1185 1185 WorkflowTransitions_087:
1186 1186 new_status_id: 2
1187 1187 role_id: 3
1188 1188 old_status_id: 6
1189 1189 id: 87
1190 1190 tracker_id: 1
1191 1191 type: WorkflowTransition
1192 1192 WorkflowTransitions_168:
1193 1193 new_status_id: 3
1194 1194 role_id: 3
1195 1195 old_status_id: 4
1196 1196 id: 168
1197 1197 tracker_id: 2
1198 1198 type: WorkflowTransition
1199 1199 WorkflowTransitions_194:
1200 1200 new_status_id: 5
1201 1201 role_id: 1
1202 1202 old_status_id: 3
1203 1203 id: 194
1204 1204 tracker_id: 3
1205 1205 type: WorkflowTransition
1206 1206 WorkflowTransitions_088:
1207 1207 new_status_id: 3
1208 1208 role_id: 3
1209 1209 old_status_id: 6
1210 1210 id: 88
1211 1211 tracker_id: 1
1212 1212 type: WorkflowTransition
1213 1213 WorkflowTransitions_169:
1214 1214 new_status_id: 5
1215 1215 role_id: 3
1216 1216 old_status_id: 4
1217 1217 id: 169
1218 1218 tracker_id: 2
1219 1219 type: WorkflowTransition
1220 1220 WorkflowTransitions_195:
1221 1221 new_status_id: 6
1222 1222 role_id: 1
1223 1223 old_status_id: 3
1224 1224 id: 195
1225 1225 tracker_id: 3
1226 1226 type: WorkflowTransition
1227 1227 WorkflowTransitions_089:
1228 1228 new_status_id: 4
1229 1229 role_id: 3
1230 1230 old_status_id: 6
1231 1231 id: 89
1232 1232 tracker_id: 1
1233 1233 type: WorkflowTransition
1234 1234 WorkflowTransitions_196:
1235 1235 new_status_id: 1
1236 1236 role_id: 1
1237 1237 old_status_id: 4
1238 1238 id: 196
1239 1239 tracker_id: 3
1240 1240 type: WorkflowTransition
1241 1241 WorkflowTransitions_197:
1242 1242 new_status_id: 2
1243 1243 role_id: 1
1244 1244 old_status_id: 4
1245 1245 id: 197
1246 1246 tracker_id: 3
1247 1247 type: WorkflowTransition
1248 1248 WorkflowTransitions_198:
1249 1249 new_status_id: 3
1250 1250 role_id: 1
1251 1251 old_status_id: 4
1252 1252 id: 198
1253 1253 tracker_id: 3
1254 1254 type: WorkflowTransition
1255 1255 WorkflowTransitions_199:
1256 1256 new_status_id: 5
1257 1257 role_id: 1
1258 1258 old_status_id: 4
1259 1259 id: 199
1260 1260 tracker_id: 3
1261 1261 type: WorkflowTransition
1262 1262 WorkflowTransitions_010:
1263 1263 new_status_id: 6
1264 1264 role_id: 1
1265 1265 old_status_id: 2
1266 1266 id: 10
1267 1267 tracker_id: 1
1268 1268 type: WorkflowTransition
1269 1269 WorkflowTransitions_011:
1270 1270 new_status_id: 1
1271 1271 role_id: 1
1272 1272 old_status_id: 3
1273 1273 id: 11
1274 1274 tracker_id: 1
1275 1275 type: WorkflowTransition
1276 1276 WorkflowTransitions_012:
1277 1277 new_status_id: 2
1278 1278 role_id: 1
1279 1279 old_status_id: 3
1280 1280 id: 12
1281 1281 tracker_id: 1
1282 1282 type: WorkflowTransition
1283 1283 WorkflowTransitions_200:
1284 1284 new_status_id: 6
1285 1285 role_id: 1
1286 1286 old_status_id: 4
1287 1287 id: 200
1288 1288 tracker_id: 3
1289 1289 type: WorkflowTransition
1290 1290 WorkflowTransitions_013:
1291 1291 new_status_id: 4
1292 1292 role_id: 1
1293 1293 old_status_id: 3
1294 1294 id: 13
1295 1295 tracker_id: 1
1296 1296 type: WorkflowTransition
1297 1297 WorkflowTransitions_120:
1298 1298 new_status_id: 5
1299 1299 role_id: 1
1300 1300 old_status_id: 6
1301 1301 id: 120
1302 1302 tracker_id: 2
1303 1303 type: WorkflowTransition
1304 1304 WorkflowTransitions_201:
1305 1305 new_status_id: 1
1306 1306 role_id: 1
1307 1307 old_status_id: 5
1308 1308 id: 201
1309 1309 tracker_id: 3
1310 1310 type: WorkflowTransition
1311 1311 WorkflowTransitions_040:
1312 1312 new_status_id: 6
1313 1313 role_id: 2
1314 1314 old_status_id: 2
1315 1315 id: 40
1316 1316 tracker_id: 1
1317 1317 type: WorkflowTransition
1318 1318 WorkflowTransitions_121:
1319 1319 new_status_id: 2
1320 1320 role_id: 2
1321 1321 old_status_id: 1
1322 1322 id: 121
1323 1323 tracker_id: 2
1324 1324 type: WorkflowTransition
1325 1325 WorkflowTransitions_202:
1326 1326 new_status_id: 2
1327 1327 role_id: 1
1328 1328 old_status_id: 5
1329 1329 id: 202
1330 1330 tracker_id: 3
1331 1331 type: WorkflowTransition
1332 1332 WorkflowTransitions_014:
1333 1333 new_status_id: 5
1334 1334 role_id: 1
1335 1335 old_status_id: 3
1336 1336 id: 14
1337 1337 tracker_id: 1
1338 1338 type: WorkflowTransition
1339 1339 WorkflowTransitions_041:
1340 1340 new_status_id: 1
1341 1341 role_id: 2
1342 1342 old_status_id: 3
1343 1343 id: 41
1344 1344 tracker_id: 1
1345 1345 type: WorkflowTransition
1346 1346 WorkflowTransitions_122:
1347 1347 new_status_id: 3
1348 1348 role_id: 2
1349 1349 old_status_id: 1
1350 1350 id: 122
1351 1351 tracker_id: 2
1352 1352 type: WorkflowTransition
1353 1353 WorkflowTransitions_203:
1354 1354 new_status_id: 3
1355 1355 role_id: 1
1356 1356 old_status_id: 5
1357 1357 id: 203
1358 1358 tracker_id: 3
1359 1359 type: WorkflowTransition
1360 1360 WorkflowTransitions_015:
1361 1361 new_status_id: 6
1362 1362 role_id: 1
1363 1363 old_status_id: 3
1364 1364 id: 15
1365 1365 tracker_id: 1
1366 1366 type: WorkflowTransition
1367 1367 WorkflowTransitions_230:
1368 1368 new_status_id: 6
1369 1369 role_id: 2
1370 1370 old_status_id: 4
1371 1371 id: 230
1372 1372 tracker_id: 3
1373 1373 type: WorkflowTransition
1374 1374 WorkflowTransitions_123:
1375 1375 new_status_id: 4
1376 1376 role_id: 2
1377 1377 old_status_id: 1
1378 1378 id: 123
1379 1379 tracker_id: 2
1380 1380 type: WorkflowTransition
1381 1381 WorkflowTransitions_204:
1382 1382 new_status_id: 4
1383 1383 role_id: 1
1384 1384 old_status_id: 5
1385 1385 id: 204
1386 1386 tracker_id: 3
1387 1387 type: WorkflowTransition
1388 1388 WorkflowTransitions_016:
1389 1389 new_status_id: 1
1390 1390 role_id: 1
1391 1391 old_status_id: 4
1392 1392 id: 16
1393 1393 tracker_id: 1
1394 1394 type: WorkflowTransition
1395 1395 WorkflowTransitions_042:
1396 1396 new_status_id: 2
1397 1397 role_id: 2
1398 1398 old_status_id: 3
1399 1399 id: 42
1400 1400 tracker_id: 1
1401 1401 type: WorkflowTransition
1402 1402 WorkflowTransitions_231:
1403 1403 new_status_id: 1
1404 1404 role_id: 2
1405 1405 old_status_id: 5
1406 1406 id: 231
1407 1407 tracker_id: 3
1408 1408 type: WorkflowTransition
1409 1409 WorkflowTransitions_070:
1410 1410 new_status_id: 6
1411 1411 role_id: 3
1412 1412 old_status_id: 2
1413 1413 id: 70
1414 1414 tracker_id: 1
1415 1415 type: WorkflowTransition
1416 1416 WorkflowTransitions_124:
1417 1417 new_status_id: 5
1418 1418 role_id: 2
1419 1419 old_status_id: 1
1420 1420 id: 124
1421 1421 tracker_id: 2
1422 1422 type: WorkflowTransition
1423 1423 WorkflowTransitions_150:
1424 1424 new_status_id: 5
1425 1425 role_id: 2
1426 1426 old_status_id: 6
1427 1427 id: 150
1428 1428 tracker_id: 2
1429 1429 type: WorkflowTransition
1430 1430 WorkflowTransitions_205:
1431 1431 new_status_id: 6
1432 1432 role_id: 1
1433 1433 old_status_id: 5
1434 1434 id: 205
1435 1435 tracker_id: 3
1436 1436 type: WorkflowTransition
1437 1437 WorkflowTransitions_017:
1438 1438 new_status_id: 2
1439 1439 role_id: 1
1440 1440 old_status_id: 4
1441 1441 id: 17
1442 1442 tracker_id: 1
1443 1443 type: WorkflowTransition
1444 1444 WorkflowTransitions_043:
1445 1445 new_status_id: 4
1446 1446 role_id: 2
1447 1447 old_status_id: 3
1448 1448 id: 43
1449 1449 tracker_id: 1
1450 1450 type: WorkflowTransition
1451 1451 WorkflowTransitions_232:
1452 1452 new_status_id: 2
1453 1453 role_id: 2
1454 1454 old_status_id: 5
1455 1455 id: 232
1456 1456 tracker_id: 3
1457 1457 type: WorkflowTransition
1458 1458 WorkflowTransitions_125:
1459 1459 new_status_id: 6
1460 1460 role_id: 2
1461 1461 old_status_id: 1
1462 1462 id: 125
1463 1463 tracker_id: 2
1464 1464 type: WorkflowTransition
1465 1465 WorkflowTransitions_151:
1466 1466 new_status_id: 2
1467 1467 role_id: 3
1468 1468 old_status_id: 1
1469 1469 id: 151
1470 1470 tracker_id: 2
1471 1471 type: WorkflowTransition
1472 1472 WorkflowTransitions_206:
1473 1473 new_status_id: 1
1474 1474 role_id: 1
1475 1475 old_status_id: 6
1476 1476 id: 206
1477 1477 tracker_id: 3
1478 1478 type: WorkflowTransition
1479 1479 WorkflowTransitions_018:
1480 1480 new_status_id: 3
1481 1481 role_id: 1
1482 1482 old_status_id: 4
1483 1483 id: 18
1484 1484 tracker_id: 1
1485 1485 type: WorkflowTransition
1486 1486 WorkflowTransitions_044:
1487 1487 new_status_id: 5
1488 1488 role_id: 2
1489 1489 old_status_id: 3
1490 1490 id: 44
1491 1491 tracker_id: 1
1492 1492 type: WorkflowTransition
1493 1493 WorkflowTransitions_071:
1494 1494 new_status_id: 1
1495 1495 role_id: 3
1496 1496 old_status_id: 3
1497 1497 id: 71
1498 1498 tracker_id: 1
1499 1499 type: WorkflowTransition
1500 1500 WorkflowTransitions_233:
1501 1501 new_status_id: 3
1502 1502 role_id: 2
1503 1503 old_status_id: 5
1504 1504 id: 233
1505 1505 tracker_id: 3
1506 1506 type: WorkflowTransition
1507 1507 WorkflowTransitions_126:
1508 1508 new_status_id: 1
1509 1509 role_id: 2
1510 1510 old_status_id: 2
1511 1511 id: 126
1512 1512 tracker_id: 2
1513 1513 type: WorkflowTransition
1514 1514 WorkflowTransitions_152:
1515 1515 new_status_id: 3
1516 1516 role_id: 3
1517 1517 old_status_id: 1
1518 1518 id: 152
1519 1519 tracker_id: 2
1520 1520 type: WorkflowTransition
1521 1521 WorkflowTransitions_207:
1522 1522 new_status_id: 2
1523 1523 role_id: 1
1524 1524 old_status_id: 6
1525 1525 id: 207
1526 1526 tracker_id: 3
1527 1527 type: WorkflowTransition
1528 1528 WorkflowTransitions_019:
1529 1529 new_status_id: 5
1530 1530 role_id: 1
1531 1531 old_status_id: 4
1532 1532 id: 19
1533 1533 tracker_id: 1
1534 1534 type: WorkflowTransition
1535 1535 WorkflowTransitions_045:
1536 1536 new_status_id: 6
1537 1537 role_id: 2
1538 1538 old_status_id: 3
1539 1539 id: 45
1540 1540 tracker_id: 1
1541 1541 type: WorkflowTransition
1542 1542 WorkflowTransitions_260:
1543 1543 new_status_id: 6
1544 1544 role_id: 3
1545 1545 old_status_id: 4
1546 1546 id: 260
1547 1547 tracker_id: 3
1548 1548 type: WorkflowTransition
1549 1549 WorkflowTransitions_234:
1550 1550 new_status_id: 4
1551 1551 role_id: 2
1552 1552 old_status_id: 5
1553 1553 id: 234
1554 1554 tracker_id: 3
1555 1555 type: WorkflowTransition
1556 1556 WorkflowTransitions_127:
1557 1557 new_status_id: 3
1558 1558 role_id: 2
1559 1559 old_status_id: 2
1560 1560 id: 127
1561 1561 tracker_id: 2
1562 1562 type: WorkflowTransition
1563 1563 WorkflowTransitions_153:
1564 1564 new_status_id: 4
1565 1565 role_id: 3
1566 1566 old_status_id: 1
1567 1567 id: 153
1568 1568 tracker_id: 2
1569 1569 type: WorkflowTransition
1570 1570 WorkflowTransitions_180:
1571 1571 new_status_id: 5
1572 1572 role_id: 3
1573 1573 old_status_id: 6
1574 1574 id: 180
1575 1575 tracker_id: 2
1576 1576 type: WorkflowTransition
1577 1577 WorkflowTransitions_208:
1578 1578 new_status_id: 3
1579 1579 role_id: 1
1580 1580 old_status_id: 6
1581 1581 id: 208
1582 1582 tracker_id: 3
1583 1583 type: WorkflowTransition
1584 1584 WorkflowTransitions_046:
1585 1585 new_status_id: 1
1586 1586 role_id: 2
1587 1587 old_status_id: 4
1588 1588 id: 46
1589 1589 tracker_id: 1
1590 1590 type: WorkflowTransition
1591 1591 WorkflowTransitions_072:
1592 1592 new_status_id: 2
1593 1593 role_id: 3
1594 1594 old_status_id: 3
1595 1595 id: 72
1596 1596 tracker_id: 1
1597 1597 type: WorkflowTransition
1598 1598 WorkflowTransitions_261:
1599 1599 new_status_id: 1
1600 1600 role_id: 3
1601 1601 old_status_id: 5
1602 1602 id: 261
1603 1603 tracker_id: 3
1604 1604 type: WorkflowTransition
1605 1605 WorkflowTransitions_235:
1606 1606 new_status_id: 6
1607 1607 role_id: 2
1608 1608 old_status_id: 5
1609 1609 id: 235
1610 1610 tracker_id: 3
1611 1611 type: WorkflowTransition
1612 1612 WorkflowTransitions_154:
1613 1613 new_status_id: 5
1614 1614 role_id: 3
1615 1615 old_status_id: 1
1616 1616 id: 154
1617 1617 tracker_id: 2
1618 1618 type: WorkflowTransition
1619 1619 WorkflowTransitions_181:
1620 1620 new_status_id: 2
1621 1621 role_id: 1
1622 1622 old_status_id: 1
1623 1623 id: 181
1624 1624 tracker_id: 3
1625 1625 type: WorkflowTransition
1626 1626 WorkflowTransitions_209:
1627 1627 new_status_id: 4
1628 1628 role_id: 1
1629 1629 old_status_id: 6
1630 1630 id: 209
1631 1631 tracker_id: 3
1632 1632 type: WorkflowTransition
1633 1633 WorkflowTransitions_047:
1634 1634 new_status_id: 2
1635 1635 role_id: 2
1636 1636 old_status_id: 4
1637 1637 id: 47
1638 1638 tracker_id: 1
1639 1639 type: WorkflowTransition
1640 1640 WorkflowTransitions_073:
1641 1641 new_status_id: 4
1642 1642 role_id: 3
1643 1643 old_status_id: 3
1644 1644 id: 73
1645 1645 tracker_id: 1
1646 1646 type: WorkflowTransition
1647 1647 WorkflowTransitions_128:
1648 1648 new_status_id: 4
1649 1649 role_id: 2
1650 1650 old_status_id: 2
1651 1651 id: 128
1652 1652 tracker_id: 2
1653 1653 type: WorkflowTransition
1654 1654 WorkflowTransitions_262:
1655 1655 new_status_id: 2
1656 1656 role_id: 3
1657 1657 old_status_id: 5
1658 1658 id: 262
1659 1659 tracker_id: 3
1660 1660 type: WorkflowTransition
1661 1661 WorkflowTransitions_236:
1662 1662 new_status_id: 1
1663 1663 role_id: 2
1664 1664 old_status_id: 6
1665 1665 id: 236
1666 1666 tracker_id: 3
1667 1667 type: WorkflowTransition
1668 1668 WorkflowTransitions_155:
1669 1669 new_status_id: 6
1670 1670 role_id: 3
1671 1671 old_status_id: 1
1672 1672 id: 155
1673 1673 tracker_id: 2
1674 1674 type: WorkflowTransition
1675 1675 WorkflowTransitions_048:
1676 1676 new_status_id: 3
1677 1677 role_id: 2
1678 1678 old_status_id: 4
1679 1679 id: 48
1680 1680 tracker_id: 1
1681 1681 type: WorkflowTransition
1682 1682 WorkflowTransitions_074:
1683 1683 new_status_id: 5
1684 1684 role_id: 3
1685 1685 old_status_id: 3
1686 1686 id: 74
1687 1687 tracker_id: 1
1688 1688 type: WorkflowTransition
1689 1689 WorkflowTransitions_129:
1690 1690 new_status_id: 5
1691 1691 role_id: 2
1692 1692 old_status_id: 2
1693 1693 id: 129
1694 1694 tracker_id: 2
1695 1695 type: WorkflowTransition
1696 1696 WorkflowTransitions_263:
1697 1697 new_status_id: 3
1698 1698 role_id: 3
1699 1699 old_status_id: 5
1700 1700 id: 263
1701 1701 tracker_id: 3
1702 1702 type: WorkflowTransition
1703 1703 WorkflowTransitions_237:
1704 1704 new_status_id: 2
1705 1705 role_id: 2
1706 1706 old_status_id: 6
1707 1707 id: 237
1708 1708 tracker_id: 3
1709 1709 type: WorkflowTransition
1710 1710 WorkflowTransitions_182:
1711 1711 new_status_id: 3
1712 1712 role_id: 1
1713 1713 old_status_id: 1
1714 1714 id: 182
1715 1715 tracker_id: 3
1716 1716 type: WorkflowTransition
1717 1717 WorkflowTransitions_049:
1718 1718 new_status_id: 5
1719 1719 role_id: 2
1720 1720 old_status_id: 4
1721 1721 id: 49
1722 1722 tracker_id: 1
1723 1723 type: WorkflowTransition
1724 1724 WorkflowTransitions_075:
1725 1725 new_status_id: 6
1726 1726 role_id: 3
1727 1727 old_status_id: 3
1728 1728 id: 75
1729 1729 tracker_id: 1
1730 1730 type: WorkflowTransition
1731 1731 WorkflowTransitions_156:
1732 1732 new_status_id: 1
1733 1733 role_id: 3
1734 1734 old_status_id: 2
1735 1735 id: 156
1736 1736 tracker_id: 2
1737 1737 type: WorkflowTransition
1738 1738 WorkflowTransitions_264:
1739 1739 new_status_id: 4
1740 1740 role_id: 3
1741 1741 old_status_id: 5
1742 1742 id: 264
1743 1743 tracker_id: 3
1744 1744 type: WorkflowTransition
1745 1745 WorkflowTransitions_238:
1746 1746 new_status_id: 3
1747 1747 role_id: 2
1748 1748 old_status_id: 6
1749 1749 id: 238
1750 1750 tracker_id: 3
1751 1751 type: WorkflowTransition
1752 1752 WorkflowTransitions_183:
1753 1753 new_status_id: 4
1754 1754 role_id: 1
1755 1755 old_status_id: 1
1756 1756 id: 183
1757 1757 tracker_id: 3
1758 1758 type: WorkflowTransition
1759 1759 WorkflowTransitions_076:
1760 1760 new_status_id: 1
1761 1761 role_id: 3
1762 1762 old_status_id: 4
1763 1763 id: 76
1764 1764 tracker_id: 1
1765 1765 type: WorkflowTransition
1766 1766 WorkflowTransitions_157:
1767 1767 new_status_id: 3
1768 1768 role_id: 3
1769 1769 old_status_id: 2
1770 1770 id: 157
1771 1771 tracker_id: 2
1772 1772 type: WorkflowTransition
1773 1773 WorkflowTransitions_265:
1774 1774 new_status_id: 6
1775 1775 role_id: 3
1776 1776 old_status_id: 5
1777 1777 id: 265
1778 1778 tracker_id: 3
1779 1779 type: WorkflowTransition
1780 1780 WorkflowTransitions_239:
1781 1781 new_status_id: 4
1782 1782 role_id: 2
1783 1783 old_status_id: 6
1784 1784 id: 239
1785 1785 tracker_id: 3
1786 1786 type: WorkflowTransition
1787 1787 WorkflowTransitions_077:
1788 1788 new_status_id: 2
1789 1789 role_id: 3
1790 1790 old_status_id: 4
1791 1791 id: 77
1792 1792 tracker_id: 1
1793 1793 type: WorkflowTransition
1794 1794 WorkflowTransitions_158:
1795 1795 new_status_id: 4
1796 1796 role_id: 3
1797 1797 old_status_id: 2
1798 1798 id: 158
1799 1799 tracker_id: 2
1800 1800 type: WorkflowTransition
1801 1801 WorkflowTransitions_184:
1802 1802 new_status_id: 5
1803 1803 role_id: 1
1804 1804 old_status_id: 1
1805 1805 id: 184
1806 1806 tracker_id: 3
1807 1807 type: WorkflowTransition
1808 1808 WorkflowTransitions_266:
1809 1809 new_status_id: 1
1810 1810 role_id: 3
1811 1811 old_status_id: 6
1812 1812 id: 266
1813 1813 tracker_id: 3
1814 1814 type: WorkflowTransition
1815 1815 WorkflowTransitions_078:
1816 1816 new_status_id: 3
1817 1817 role_id: 3
1818 1818 old_status_id: 4
1819 1819 id: 78
1820 1820 tracker_id: 1
1821 1821 type: WorkflowTransition
1822 1822 WorkflowTransitions_159:
1823 1823 new_status_id: 5
1824 1824 role_id: 3
1825 1825 old_status_id: 2
1826 1826 id: 159
1827 1827 tracker_id: 2
1828 1828 type: WorkflowTransition
1829 1829 WorkflowTransitions_185:
1830 1830 new_status_id: 6
1831 1831 role_id: 1
1832 1832 old_status_id: 1
1833 1833 id: 185
1834 1834 tracker_id: 3
1835 1835 type: WorkflowTransition
1836 1836 WorkflowTransitions_267:
1837 1837 new_status_id: 2
1838 1838 role_id: 3
1839 1839 old_status_id: 6
1840 1840 id: 267
1841 1841 tracker_id: 3
1842 1842 type: WorkflowTransition
1843 1843 WorkflowTransitions_079:
1844 1844 new_status_id: 5
1845 1845 role_id: 3
1846 1846 old_status_id: 4
1847 1847 id: 79
1848 1848 tracker_id: 1
1849 1849 type: WorkflowTransition
1850 1850 WorkflowTransitions_186:
1851 1851 new_status_id: 1
1852 1852 role_id: 1
1853 1853 old_status_id: 2
1854 1854 id: 186
1855 1855 tracker_id: 3
1856 1856 type: WorkflowTransition
1857 1857 WorkflowTransitions_268:
1858 1858 new_status_id: 3
1859 1859 role_id: 3
1860 1860 old_status_id: 6
1861 1861 id: 268
1862 1862 tracker_id: 3
1863 1863 type: WorkflowTransition
1864 1864 WorkflowTransitions_187:
1865 1865 new_status_id: 3
1866 1866 role_id: 1
1867 1867 old_status_id: 2
1868 1868 id: 187
1869 1869 tracker_id: 3
1870 1870 type: WorkflowTransition
1871 1871 WorkflowTransitions_269:
1872 1872 new_status_id: 4
1873 1873 role_id: 3
1874 1874 old_status_id: 6
1875 1875 id: 269
1876 1876 tracker_id: 3
1877 1877 type: WorkflowTransition
1878 1878 WorkflowTransitions_188:
1879 1879 new_status_id: 4
1880 1880 role_id: 1
1881 1881 old_status_id: 2
1882 1882 id: 188
1883 1883 tracker_id: 3
1884 1884 type: WorkflowTransition
1885 WorkflowTransitions_271:
1886 new_status_id: 3
1887 role_id: 1
1888 old_status_id: 0
1889 id: 271
1890 tracker_id: 2
1891 type: WorkflowTransition
1892 WorkflowTransitions_272:
1893 new_status_id: 3
1894 role_id: 2
1895 old_status_id: 0
1896 id: 272
1897 tracker_id: 1
1898 type: WorkflowTransition
1899 WorkflowTransitions_273:
1900 new_status_id: 2
1901 role_id: 1
1902 old_status_id: 0
1903 id: 273
1904 tracker_id: 3
1905 type: WorkflowTransition
1906 WorkflowTransitions_274:
1907 new_status_id: 2
1908 role_id: 1
1909 old_status_id: 0
1910 id: 274
1911 tracker_id: 1
1912 type: WorkflowTransition
1913 WorkflowTransitions_275:
1914 new_status_id: 1
1915 role_id: 1
1916 old_status_id: 0
1917 id: 275
1918 tracker_id: 1
1919 type: WorkflowTransition
1920 WorkflowTransitions_276:
1921 new_status_id: 1
1922 role_id: 1
1923 old_status_id: 0
1924 id: 276
1925 tracker_id: 2
1926 type: WorkflowTransition
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,369 +1,393
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 WorkflowsControllerTest < ActionController::TestCase
21 21 fixtures :roles, :trackers, :workflows, :users, :issue_statuses
22 22
23 23 def setup
24 24 User.current = nil
25 25 @request.session[:user_id] = 1 # admin
26 26 end
27 27
28 28 def test_index
29 29 get :index
30 30 assert_response :success
31 31 assert_template 'index'
32 32
33 33 count = WorkflowTransition.where(:role_id => 1, :tracker_id => 2).count
34 34 assert_select 'a[href=?]', '/workflows/edit?role_id=1&tracker_id=2', :content => count.to_s
35 35 end
36 36
37 37 def test_get_edit
38 38 get :edit
39 39 assert_response :success
40 40 assert_template 'edit'
41 41 end
42 42
43 43 def test_get_edit_with_role_and_tracker
44 44 WorkflowTransition.delete_all
45 45 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
46 46 WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
47 47
48 48 get :edit, :role_id => 2, :tracker_id => 1
49 49 assert_response :success
50 50 assert_template 'edit'
51 51
52 52 # used status only
53 53 assert_not_nil assigns(:statuses)
54 54 assert_equal [2, 3, 5], assigns(:statuses).collect(&:id)
55 55
56 56 # allowed transitions
57 57 assert_select 'input[type=checkbox][name=?][value="1"][checked=checked]', 'transitions[3][5][always]'
58 58 # not allowed
59 59 assert_select 'input[type=checkbox][name=?][value="1"]:not([checked=checked])', 'transitions[3][2][always]'
60 60 # unused
61 61 assert_select 'input[type=checkbox][name=?]', 'transitions[1][1][always]', 0
62 62 end
63 63
64 def test_get_edit_should_include_allowed_statuses_for_new_issues
65 WorkflowTransition.delete_all
66 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 0, :new_status_id => 1)
67
68 get :edit, :role_id => 1, :tracker_id => 1
69 assert_response :success
70 assert_select 'td', 'New issue'
71 assert_select 'input[type=checkbox][name=?][value="1"][checked=checked]', 'transitions[0][1][always]'
72 end
73
64 74 def test_get_edit_with_all_roles_and_all_trackers
65 75 get :edit, :role_id => 'all', :tracker_id => 'all'
66 76 assert_response :success
67 77 assert_equal Role.sorted.to_a, assigns(:roles)
68 78 assert_equal Tracker.sorted.to_a, assigns(:trackers)
69 79 end
70 80
71 81 def test_get_edit_with_role_and_tracker_and_all_statuses
72 82 WorkflowTransition.delete_all
73 83
74 84 get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0'
75 85 assert_response :success
76 86 assert_template 'edit'
77 87
78 88 assert_not_nil assigns(:statuses)
79 89 assert_equal IssueStatus.count, assigns(:statuses).size
80 90
81 91 assert_select 'input[type=checkbox][name=?]', 'transitions[1][1][always]'
82 92 end
83 93
84 94 def test_post_edit
85 95 WorkflowTransition.delete_all
86 96
87 97 post :edit, :role_id => 2, :tracker_id => 1,
88 98 :transitions => {
89 99 '4' => {'5' => {'always' => '1'}},
90 100 '3' => {'1' => {'always' => '1'}, '2' => {'always' => '1'}}
91 101 }
92 102 assert_response 302
93 103
94 104 assert_equal 3, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count
95 105 assert_not_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first
96 106 assert_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4).first
97 107 end
98 108
109 def test_post_edit_with_allowed_statuses_for_new_issues
110 WorkflowTransition.delete_all
111
112 post :edit, :role_id => 2, :tracker_id => 1,
113 :transitions => {
114 '0' => {'1' => {'always' => '1'}, '2' => {'always' => '1'}}
115 }
116 assert_response 302
117
118 assert WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 0, :new_status_id => 1).any?
119 assert WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 0, :new_status_id => 2).any?
120 assert_equal 2, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count
121 end
122
99 123 def test_post_edit_with_additional_transitions
100 124 WorkflowTransition.delete_all
101 125
102 126 post :edit, :role_id => 2, :tracker_id => 1,
103 127 :transitions => {
104 128 '4' => {'5' => {'always' => '1', 'author' => '0', 'assignee' => '0'}},
105 129 '3' => {'1' => {'always' => '0', 'author' => '1', 'assignee' => '0'},
106 130 '2' => {'always' => '0', 'author' => '0', 'assignee' => '1'},
107 131 '4' => {'always' => '0', 'author' => '1', 'assignee' => '1'}}
108 132 }
109 133 assert_response 302
110 134
111 135 assert_equal 4, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count
112 136
113 137 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5).first
114 138 assert ! w.author
115 139 assert ! w.assignee
116 140 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1).first
117 141 assert w.author
118 142 assert ! w.assignee
119 143 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first
120 144 assert ! w.author
121 145 assert w.assignee
122 146 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4).first
123 147 assert w.author
124 148 assert w.assignee
125 149 end
126 150
127 151 def test_get_permissions
128 152 get :permissions
129 153
130 154 assert_response :success
131 155 assert_template 'permissions'
132 156 end
133 157
134 158 def test_get_permissions_with_role_and_tracker
135 159 WorkflowPermission.delete_all
136 160 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
137 161 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
138 162 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
139 163
140 164 get :permissions, :role_id => 1, :tracker_id => 2
141 165 assert_response :success
142 166 assert_template 'permissions'
143 167
144 168 assert_select 'input[name=?][value="1"]', 'role_id[]'
145 169 assert_select 'input[name=?][value="2"]', 'tracker_id[]'
146 170
147 171 # Required field
148 172 assert_select 'select[name=?]', 'permissions[2][assigned_to_id]' do
149 173 assert_select 'option[value=""]'
150 174 assert_select 'option[value=""][selected=selected]', 0
151 175 assert_select 'option[value=readonly]', :text => 'Read-only'
152 176 assert_select 'option[value=readonly][selected=selected]', 0
153 177 assert_select 'option[value=required]', :text => 'Required'
154 178 assert_select 'option[value=required][selected=selected]'
155 179 end
156 180
157 181 # Read-only field
158 182 assert_select 'select[name=?]', 'permissions[3][fixed_version_id]' do
159 183 assert_select 'option[value=""]'
160 184 assert_select 'option[value=""][selected=selected]', 0
161 185 assert_select 'option[value=readonly]', :text => 'Read-only'
162 186 assert_select 'option[value=readonly][selected=selected]'
163 187 assert_select 'option[value=required]', :text => 'Required'
164 188 assert_select 'option[value=required][selected=selected]', 0
165 189 end
166 190
167 191 # Other field
168 192 assert_select 'select[name=?]', 'permissions[3][due_date]' do
169 193 assert_select 'option[value=""]'
170 194 assert_select 'option[value=""][selected=selected]', 0
171 195 assert_select 'option[value=readonly]', :text => 'Read-only'
172 196 assert_select 'option[value=readonly][selected=selected]', 0
173 197 assert_select 'option[value=required]', :text => 'Required'
174 198 assert_select 'option[value=required][selected=selected]', 0
175 199 end
176 200 end
177 201
178 202 def test_get_permissions_with_required_custom_field_should_not_show_required_option
179 203 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :tracker_ids => [1], :is_required => true)
180 204
181 205 get :permissions, :role_id => 1, :tracker_id => 1
182 206 assert_response :success
183 207 assert_template 'permissions'
184 208
185 209 # Custom field that is always required
186 210 # The default option is "(Required)"
187 211 assert_select 'select[name=?]', "permissions[3][#{cf.id}]" do
188 212 assert_select 'option[value=""]'
189 213 assert_select 'option[value=readonly]', :text => 'Read-only'
190 214 assert_select 'option[value=required]', 0
191 215 end
192 216 end
193 217
194 218 def test_get_permissions_should_disable_hidden_custom_fields
195 219 cf1 = IssueCustomField.generate!(:tracker_ids => [1], :visible => true)
196 220 cf2 = IssueCustomField.generate!(:tracker_ids => [1], :visible => false, :role_ids => [1])
197 221 cf3 = IssueCustomField.generate!(:tracker_ids => [1], :visible => false, :role_ids => [1, 2])
198 222
199 223 get :permissions, :role_id => 2, :tracker_id => 1
200 224 assert_response :success
201 225 assert_template 'permissions'
202 226
203 227 assert_select 'select[name=?]:not(.disabled)', "permissions[1][#{cf1.id}]"
204 228 assert_select 'select[name=?]:not(.disabled)', "permissions[1][#{cf3.id}]"
205 229
206 230 assert_select 'select[name=?][disabled=disabled]', "permissions[1][#{cf2.id}]" do
207 231 assert_select 'option[value=""][selected=selected]', :text => 'Hidden'
208 232 end
209 233 end
210 234
211 235 def test_get_permissions_with_missing_permissions_for_roles_should_default_to_no_change
212 236 WorkflowPermission.delete_all
213 237 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
214 238
215 239 get :permissions, :role_id => [1, 2], :tracker_id => 2
216 240 assert_response :success
217 241
218 242 assert_select 'select[name=?]', 'permissions[1][assigned_to_id]' do
219 243 assert_select 'option[selected]', 1
220 244 assert_select 'option[selected][value=no_change]'
221 245 end
222 246 end
223 247
224 248 def test_get_permissions_with_different_permissions_for_roles_should_default_to_no_change
225 249 WorkflowPermission.delete_all
226 250 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
227 251 WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'readonly')
228 252
229 253 get :permissions, :role_id => [1, 2], :tracker_id => 2
230 254 assert_response :success
231 255
232 256 assert_select 'select[name=?]', 'permissions[1][assigned_to_id]' do
233 257 assert_select 'option[selected]', 1
234 258 assert_select 'option[selected][value=no_change]'
235 259 end
236 260 end
237 261
238 262 def test_get_permissions_with_same_permissions_for_roles_should_default_to_permission
239 263 WorkflowPermission.delete_all
240 264 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
241 265 WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
242 266
243 267 get :permissions, :role_id => [1, 2], :tracker_id => 2
244 268 assert_response :success
245 269
246 270 assert_select 'select[name=?]', 'permissions[1][assigned_to_id]' do
247 271 assert_select 'option[selected]', 1
248 272 assert_select 'option[selected][value=required]'
249 273 end
250 274 end
251 275
252 276 def test_get_permissions_with_role_and_tracker_and_all_statuses_should_show_all_statuses
253 277 WorkflowTransition.delete_all
254 278
255 279 get :permissions, :role_id => 1, :tracker_id => 2, :used_statuses_only => '0'
256 280 assert_response :success
257 281 assert_equal IssueStatus.sorted.to_a, assigns(:statuses)
258 282 end
259 283
260 284 def test_get_permissions_should_set_css_class
261 285 WorkflowPermission.delete_all
262 286 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
263 287
264 288 get :permissions, :role_id => 1, :tracker_id => 2
265 289 assert_response :success
266 290 assert_select 'td.required > select[name=?]', 'permissions[1][assigned_to_id]'
267 291 end
268 292
269 293 def test_post_permissions
270 294 WorkflowPermission.delete_all
271 295
272 296 post :permissions, :role_id => 1, :tracker_id => 2, :permissions => {
273 297 '1' => {'assigned_to_id' => '', 'fixed_version_id' => 'required', 'due_date' => ''},
274 298 '2' => {'assigned_to_id' => 'readonly', 'fixed_version_id' => 'readonly', 'due_date' => ''},
275 299 '3' => {'assigned_to_id' => '', 'fixed_version_id' => '', 'due_date' => ''}
276 300 }
277 301 assert_response 302
278 302
279 303 workflows = WorkflowPermission.all
280 304 assert_equal 3, workflows.size
281 305 workflows.each do |workflow|
282 306 assert_equal 1, workflow.role_id
283 307 assert_equal 2, workflow.tracker_id
284 308 end
285 309 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'assigned_to_id' && wf.rule == 'readonly'}
286 310 assert workflows.detect {|wf| wf.old_status_id == 1 && wf.field_name == 'fixed_version_id' && wf.rule == 'required'}
287 311 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'fixed_version_id' && wf.rule == 'readonly'}
288 312 end
289 313
290 314 def test_get_copy
291 315 get :copy
292 316 assert_response :success
293 317 assert_template 'copy'
294 318 assert_select 'select[name=source_tracker_id]' do
295 319 assert_select 'option[value="1"]', :text => 'Bug'
296 320 end
297 321 assert_select 'select[name=source_role_id]' do
298 322 assert_select 'option[value="2"]', :text => 'Developer'
299 323 end
300 324 assert_select 'select[name=?]', 'target_tracker_ids[]' do
301 325 assert_select 'option[value="3"]', :text => 'Support request'
302 326 end
303 327 assert_select 'select[name=?]', 'target_role_ids[]' do
304 328 assert_select 'option[value="1"]', :text => 'Manager'
305 329 end
306 330 end
307 331
308 332 def test_post_copy_one_to_one
309 333 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
310 334
311 335 post :copy, :source_tracker_id => '1', :source_role_id => '2',
312 336 :target_tracker_ids => ['3'], :target_role_ids => ['1']
313 337 assert_response 302
314 338 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
315 339 end
316 340
317 341 def test_post_copy_one_to_many
318 342 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
319 343
320 344 post :copy, :source_tracker_id => '1', :source_role_id => '2',
321 345 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
322 346 assert_response 302
323 347 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1)
324 348 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
325 349 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3)
326 350 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3)
327 351 end
328 352
329 353 def test_post_copy_many_to_many
330 354 source_t2 = status_transitions(:tracker_id => 2, :role_id => 2)
331 355 source_t3 = status_transitions(:tracker_id => 3, :role_id => 2)
332 356
333 357 post :copy, :source_tracker_id => 'any', :source_role_id => '2',
334 358 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
335 359 assert_response 302
336 360 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1)
337 361 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1)
338 362 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3)
339 363 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3)
340 364 end
341 365
342 366 def test_post_copy_with_incomplete_source_specification_should_fail
343 367 assert_no_difference 'WorkflowRule.count' do
344 368 post :copy,
345 369 :source_tracker_id => '', :source_role_id => '2',
346 370 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
347 371 assert_response 200
348 372 assert_select 'div.flash.error', :text => 'Please select a source tracker or role'
349 373 end
350 374 end
351 375
352 376 def test_post_copy_with_incomplete_target_specification_should_fail
353 377 assert_no_difference 'WorkflowRule.count' do
354 378 post :copy,
355 379 :source_tracker_id => '1', :source_role_id => '2',
356 380 :target_tracker_ids => ['2', '3']
357 381 assert_response 200
358 382 assert_select 'div.flash.error', :text => 'Please select target tracker(s) and role(s)'
359 383 end
360 384 end
361 385
362 386 # Returns an array of status transitions that can be compared
363 387 def status_transitions(conditions)
364 388 WorkflowTransition.
365 389 where(conditions).
366 390 order('tracker_id, role_id, old_status_id, new_status_id').
367 391 collect {|w| [w.old_status, w.new_status_id]}
368 392 end
369 393 end
@@ -1,141 +1,142
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 RoleTest < ActiveSupport::TestCase
21 21 fixtures :roles, :workflows, :trackers
22 22
23 23 def test_sorted_scope
24 24 assert_equal Role.all.sort, Role.sorted.to_a
25 25 end
26 26
27 27 def test_givable_scope
28 28 assert_equal Role.all.reject(&:builtin?).sort, Role.givable.to_a
29 29 end
30 30
31 31 def test_builtin_scope
32 32 assert_equal Role.all.select(&:builtin?).sort, Role.builtin(true).to_a.sort
33 33 assert_equal Role.all.reject(&:builtin?).sort, Role.builtin(false).to_a.sort
34 34 end
35 35
36 36 def test_copy_from
37 37 role = Role.find(1)
38 38 copy = Role.new.copy_from(role)
39 39
40 40 assert_nil copy.id
41 41 assert_equal '', copy.name
42 42 assert_equal role.permissions, copy.permissions
43 43
44 44 copy.name = 'Copy'
45 45 assert copy.save
46 46 end
47 47
48 48 def test_copy_workflows
49 49 source = Role.find(1)
50 assert_equal 90, source.workflow_rules.size
50 rule_count = source.workflow_rules.count
51 assert rule_count > 0
51 52
52 53 target = Role.new(:name => 'Target')
53 54 assert target.save
54 55 target.workflow_rules.copy(source)
55 56 target.reload
56 assert_equal 90, target.workflow_rules.size
57 assert_equal rule_count, target.workflow_rules.size
57 58 end
58 59
59 60 def test_permissions_should_be_unserialized_with_its_coder
60 61 Role::PermissionsAttributeCoder.stubs(:load).returns([:foo, :bar])
61 62 role = Role.find(1)
62 63 assert_equal [:foo, :bar], role.permissions
63 64 end
64 65
65 66 def test_add_permission
66 67 role = Role.find(1)
67 68 size = role.permissions.size
68 69 role.add_permission!("apermission", "anotherpermission")
69 70 role.reload
70 71 assert role.permissions.include?(:anotherpermission)
71 72 assert_equal size + 2, role.permissions.size
72 73 end
73 74
74 75 def test_remove_permission
75 76 role = Role.find(1)
76 77 size = role.permissions.size
77 78 perm = role.permissions[0..1]
78 79 role.remove_permission!(*perm)
79 80 role.reload
80 81 assert ! role.permissions.include?(perm[0])
81 82 assert_equal size - 2, role.permissions.size
82 83 end
83 84
84 85 def test_has_permission
85 86 role = Role.create!(:name => 'Test', :permissions => [:view_issues, :edit_issues])
86 87 assert_equal true, role.has_permission?(:view_issues)
87 88 assert_equal false, role.has_permission?(:delete_issues)
88 89 end
89 90
90 91 def test_has_permission_without_permissions
91 92 role = Role.create!(:name => 'Test')
92 93 assert_equal false, role.has_permission?(:delete_issues)
93 94 end
94 95
95 96 def test_name
96 97 I18n.locale = 'fr'
97 98 assert_equal 'Manager', Role.find(1).name
98 99 assert_equal 'Anonyme', Role.anonymous.name
99 100 assert_equal 'Non membre', Role.non_member.name
100 101 end
101 102
102 103 def test_find_all_givable
103 104 assert_equal Role.all.reject(&:builtin?).sort, Role.find_all_givable
104 105 end
105 106
106 107 def test_anonymous_should_return_the_anonymous_role
107 108 assert_no_difference('Role.count') do
108 109 role = Role.anonymous
109 110 assert role.builtin?
110 111 assert_equal Role::BUILTIN_ANONYMOUS, role.builtin
111 112 end
112 113 end
113 114
114 115 def test_anonymous_with_a_missing_anonymous_role_should_return_the_anonymous_role
115 116 Role.where(:builtin => Role::BUILTIN_ANONYMOUS).delete_all
116 117
117 118 assert_difference('Role.count') do
118 119 role = Role.anonymous
119 120 assert role.builtin?
120 121 assert_equal Role::BUILTIN_ANONYMOUS, role.builtin
121 122 end
122 123 end
123 124
124 125 def test_non_member_should_return_the_non_member_role
125 126 assert_no_difference('Role.count') do
126 127 role = Role.non_member
127 128 assert role.builtin?
128 129 assert_equal Role::BUILTIN_NON_MEMBER, role.builtin
129 130 end
130 131 end
131 132
132 133 def test_non_member_with_a_missing_non_member_role_should_return_the_non_member_role
133 134 Role.where(:builtin => Role::BUILTIN_NON_MEMBER).delete_all
134 135
135 136 assert_difference('Role.count') do
136 137 role = Role.non_member
137 138 assert role.builtin?
138 139 assert_equal Role::BUILTIN_NON_MEMBER, role.builtin
139 140 end
140 141 end
141 142 end
@@ -1,118 +1,119
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
20 20 class TrackerTest < ActiveSupport::TestCase
21 21 fixtures :trackers, :workflows, :issue_statuses, :roles, :issues
22 22
23 23 def test_sorted_scope
24 24 assert_equal Tracker.all.sort, Tracker.sorted.to_a
25 25 end
26 26
27 27 def test_named_scope
28 28 assert_equal Tracker.find_by_name('Feature'), Tracker.named('feature').first
29 29 end
30 30
31 31 def test_copy_workflows
32 32 source = Tracker.find(1)
33 assert_equal 89, source.workflow_rules.size
33 rules_count = source.workflow_rules.count
34 assert rules_count > 0
34 35
35 36 target = Tracker.new(:name => 'Target', :default_status_id => 1)
36 37 assert target.save
37 38 target.workflow_rules.copy(source)
38 39 target.reload
39 assert_equal 89, target.workflow_rules.size
40 assert_equal rules_count, target.workflow_rules.size
40 41 end
41 42
42 43 def test_issue_statuses
43 44 tracker = Tracker.find(1)
44 45 WorkflowTransition.delete_all
45 46 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
46 47 WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
47 48
48 49 assert_kind_of Array, tracker.issue_statuses
49 50 assert_kind_of IssueStatus, tracker.issue_statuses.first
50 51 assert_equal [2, 3, 5], Tracker.find(1).issue_statuses.collect(&:id)
51 52 end
52 53
53 54 def test_issue_statuses_empty
54 55 WorkflowTransition.delete_all("tracker_id = 1")
55 56 assert_equal [], Tracker.find(1).issue_statuses
56 57 end
57 58
58 59 def test_issue_statuses_should_be_empty_for_new_record
59 60 assert_equal [], Tracker.new.issue_statuses
60 61 end
61 62
62 63 def test_core_fields_should_be_enabled_by_default
63 64 tracker = Tracker.new
64 65 assert_equal Tracker::CORE_FIELDS, tracker.core_fields
65 66 assert_equal [], tracker.disabled_core_fields
66 67 end
67 68
68 69 def test_core_fields
69 70 tracker = Tracker.new
70 71 tracker.core_fields = %w(assigned_to_id due_date)
71 72
72 73 assert_equal %w(assigned_to_id due_date), tracker.core_fields
73 74 assert_equal Tracker::CORE_FIELDS - %w(assigned_to_id due_date), tracker.disabled_core_fields
74 75 end
75 76
76 77 def test_core_fields_should_return_fields_enabled_for_any_tracker
77 78 trackers = []
78 79 trackers << Tracker.new(:core_fields => %w(assigned_to_id due_date))
79 80 trackers << Tracker.new(:core_fields => %w(assigned_to_id done_ratio))
80 81 trackers << Tracker.new(:core_fields => [])
81 82
82 83 assert_equal %w(assigned_to_id due_date done_ratio), Tracker.core_fields(trackers)
83 84 assert_equal Tracker::CORE_FIELDS - %w(assigned_to_id due_date done_ratio), Tracker.disabled_core_fields(trackers)
84 85 end
85 86
86 87 def test_core_fields_should_return_all_fields_for_an_empty_argument
87 88 assert_equal Tracker::CORE_FIELDS, Tracker.core_fields([])
88 89 assert_equal [], Tracker.disabled_core_fields([])
89 90 end
90 91
91 92 def test_sort_should_sort_by_position
92 93 a = Tracker.new(:name => 'Tracker A', :position => 2)
93 94 b = Tracker.new(:name => 'Tracker B', :position => 1)
94 95
95 96 assert_equal [b, a], [a, b].sort
96 97 end
97 98
98 99 def test_destroying_a_tracker_without_issues_should_not_raise_an_error
99 100 tracker = Tracker.find(1)
100 101 Issue.delete_all :tracker_id => tracker.id
101 102
102 103 assert_difference 'Tracker.count', -1 do
103 104 assert_nothing_raised do
104 105 tracker.destroy
105 106 end
106 107 end
107 108 end
108 109
109 110 def test_destroying_a_tracker_with_issues_should_raise_an_error
110 111 tracker = Tracker.find(1)
111 112
112 113 assert_no_difference 'Tracker.count' do
113 114 assert_raise Exception do
114 115 tracker.destroy
115 116 end
116 117 end
117 118 end
118 119 end
General Comments 0
You need to be logged in to leave comments. Login now