##// END OF EJS Templates
Workflow enhancement: editable and required fields configurable by role, tracker and status (#703, #3521)....
Jean-Philippe Lang -
r9794:d7b669e50b1c
parent child
Show More

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

@@ -0,0 +1,29
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class WorkflowPermission < WorkflowRule
19 validates_inclusion_of :rule, :in => %w(readonly required)
20 validate :validate_field_name
21
22 protected
23
24 def validate_field_name
25 unless Tracker::CORE_FIELDS_ALL.include?(field_name) || field_name.to_s.match(/^\d+$/)
26 errors.add :field_name, :invalid
27 end
28 end
29 end
@@ -0,0 +1,39
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class WorkflowTransition < WorkflowRule
19 validates_presence_of :new_status
20
21 # Returns workflow transitions count by tracker and role
22 def self.count_by_tracker_and_role
23 counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{table_name} WHERE type = 'WorkflowTransition' GROUP BY role_id, tracker_id")
24 roles = Role.sorted.all
25 trackers = Tracker.sorted.all
26
27 result = []
28 trackers.each do |tracker|
29 t = []
30 roles.each do |role|
31 row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s}
32 t << [role, (row.nil? ? 0 : row['c'].to_i)]
33 end
34 result << [tracker, t]
35 end
36
37 result
38 end
39 end
@@ -0,0 +1,91
1 <%= render :partial => 'action_menu' %>
2
3 <h2><%=l(:label_workflow)%></h2>
4
5 <div class="tabs">
6 <ul>
7 <li><%= link_to 'Status transitions', {:action => 'edit', :role_id => @role, :tracker_id => @tracker} %></li>
8 <li><%= link_to 'Fields permissions', {:action => 'permissions', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %></li>
9 </ul>
10 </div>
11
12 <p><%=l(:text_workflow_edit)%>:</p>
13
14 <%= form_tag({}, :method => 'get') do %>
15 <p>
16 <label><%=l(:label_role)%>:
17 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %></label>
18
19 <label><%=l(:label_tracker)%>:
20 <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %></label>
21
22 <%= submit_tag l(:button_edit), :name => nil %>
23 </p>
24 <% end %>
25
26 <% if @tracker && @role && @statuses.any? %>
27 <%= form_tag({}, :id => 'workflow_form' ) do %>
28 <%= hidden_field_tag 'tracker_id', @tracker.id %>
29 <%= hidden_field_tag 'role_id', @role.id %>
30 <div class="autoscroll">
31 <table class="list fields_permissions">
32 <thead>
33 <tr>
34 <th align="left">
35 </th>
36 <th align="center" colspan="<%= @statuses.length %>"><%=l(:label_issue_status)%></th>
37 </tr>
38 <tr>
39 <td></td>
40 <% for status in @statuses %>
41 <td width="<%= 75 / @statuses.size %>%" align="center">
42 <%=h status.name %>
43 </td>
44 <% end %>
45 </tr>
46 </thead>
47 <tbody>
48 <tr class="group open">
49 <td colspan="<%= @statuses.size + 1 %>">
50 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
51 <%= l(:field_core_fields) %>
52 </td>
53 </tr>
54 <% @fields.each do |field, name| %>
55 <tr class="<%= cycle("odd", "even") %>">
56 <td>
57 <%=h name %>
58 </td>
59 <% for status in @statuses -%>
60 <td align="center" class="<%= @permissions[status.id][field] %>">
61 <%= field_permission_tag(@permissions, status, field) %>
62 </td>
63 <% end -%>
64 </tr>
65 <% end %>
66 <% if @custom_fields.any? %>
67 <tr class="group open">
68 <td colspan="<%= @statuses.size + 1 %>">
69 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
70 <%= l(:label_custom_field_plural) %>
71 </td>
72 </tr>
73 <% @custom_fields.each do |field| %>
74 <tr class="<%= cycle("odd", "even") %>">
75 <td>
76 <%=h field.name %>
77 </td>
78 <% for status in @statuses -%>
79 <td align="center" class="<%= @permissions[status.id][field.id.to_s] %>">
80 <%= field_permission_tag(@permissions, status, field) %>
81 </td>
82 <% end -%>
83 </tr>
84 <% end %>
85 <% end %>
86 </tbody>
87 </table>
88 </div>
89 <%= submit_tag l(:button_save) %>
90 <% end %>
91 <% end %>
@@ -0,0 +1,9
1 class AddWorkflowsType < ActiveRecord::Migration
2 def up
3 add_column :workflows, :type, :string, :limit => 30
4 end
5
6 def down
7 remove_column :workflows, :type
8 end
9 end
@@ -0,0 +1,9
1 class UpdateWorkflowsToSti < ActiveRecord::Migration
2 def up
3 WorkflowRule.update_all "type = 'WorkflowTransition'"
4 end
5
6 def down
7 WorkflowRule.update_all "type = NULL"
8 end
9 end
@@ -0,0 +1,11
1 class AddWorkflowsRuleFields < ActiveRecord::Migration
2 def up
3 add_column :workflows, :field_name, :string, :limit => 30
4 add_column :workflows, :rule, :string, :limit => 30
5 end
6
7 def down
8 remove_column :workflows, :field_name
9 remove_column :workflows, :rule
10 end
11 end
@@ -1,441 +1,437
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 :find_project, :only => [:new, :create]
25 25 before_filter :authorize, :except => [:index]
26 26 before_filter :find_optional_project, :only => [:index]
27 27 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 28 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 29 accept_rss_auth :index, :show
30 30 accept_api_auth :index, :show, :create, :update, :destroy
31 31
32 32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 33
34 34 helper :journals
35 35 helper :projects
36 36 include ProjectsHelper
37 37 helper :custom_fields
38 38 include CustomFieldsHelper
39 39 helper :issue_relations
40 40 include IssueRelationsHelper
41 41 helper :watchers
42 42 include WatchersHelper
43 43 helper :attachments
44 44 include AttachmentsHelper
45 45 helper :queries
46 46 include QueriesHelper
47 47 helper :repositories
48 48 include RepositoriesHelper
49 49 helper :sort
50 50 include SortHelper
51 51 include IssuesHelper
52 52 helper :timelog
53 53 include Redmine::Export::PDF
54 54
55 55 def index
56 56 retrieve_query
57 57 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
58 58 sort_update(@query.sortable_columns)
59 59
60 60 if @query.valid?
61 61 case params[:format]
62 62 when 'csv', 'pdf'
63 63 @limit = Setting.issues_export_limit.to_i
64 64 when 'atom'
65 65 @limit = Setting.feeds_limit.to_i
66 66 when 'xml', 'json'
67 67 @offset, @limit = api_offset_and_limit
68 68 else
69 69 @limit = per_page_option
70 70 end
71 71
72 72 @issue_count = @query.issue_count
73 73 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
74 74 @offset ||= @issue_pages.current.offset
75 75 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
76 76 :order => sort_clause,
77 77 :offset => @offset,
78 78 :limit => @limit)
79 79 @issue_count_by_group = @query.issue_count_by_group
80 80
81 81 respond_to do |format|
82 82 format.html { render :template => 'issues/index', :layout => !request.xhr? }
83 83 format.api {
84 84 Issue.load_relations(@issues) if include_in_api_response?('relations')
85 85 }
86 86 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
87 87 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
88 88 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
89 89 end
90 90 else
91 91 respond_to do |format|
92 92 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
93 93 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
94 94 format.api { render_validation_errors(@query) }
95 95 end
96 96 end
97 97 rescue ActiveRecord::RecordNotFound
98 98 render_404
99 99 end
100 100
101 101 def show
102 102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
103 103 @journals.each_with_index {|j,i| j.indice = i+1}
104 104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
105 105
106 106 @changesets = @issue.changesets.visible.all
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 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
112 112 @priorities = IssuePriority.active
113 113 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
114 114 respond_to do |format|
115 115 format.html {
116 116 retrieve_previous_and_next_issue_ids
117 117 render :template => 'issues/show'
118 118 }
119 119 format.api
120 120 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
121 121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
122 122 end
123 123 end
124 124
125 125 # Add a new issue
126 126 # The new issue will be created from an existing one if copy_from parameter is given
127 127 def new
128 128 respond_to do |format|
129 129 format.html { render :action => 'new', :layout => !request.xhr? }
130 130 format.js {
131 131 render(:update) { |page|
132 if params[:project_change]
133 page.replace_html 'all_attributes', :partial => 'form'
134 else
135 page.replace_html 'attributes', :partial => 'attributes'
136 end
132 page.replace_html 'all_attributes', :partial => 'form'
137 133 m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
138 134 page << "if ($('log_time')) {Element.#{m}('log_time');}"
139 135 }
140 136 }
141 137 end
142 138 end
143 139
144 140 def create
145 141 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
146 142 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
147 143 if @issue.save
148 144 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
149 145 respond_to do |format|
150 146 format.html {
151 147 render_attachment_warning_if_needed(@issue)
152 148 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue)))
153 149 redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
154 150 { :action => 'show', :id => @issue })
155 151 }
156 152 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
157 153 end
158 154 return
159 155 else
160 156 respond_to do |format|
161 157 format.html { render :action => 'new' }
162 158 format.api { render_validation_errors(@issue) }
163 159 end
164 160 end
165 161 end
166 162
167 163 def edit
168 164 return unless update_issue_from_params
169 165
170 166 respond_to do |format|
171 167 format.html { }
172 168 format.xml { }
173 169 end
174 170 end
175 171
176 172 def update
177 173 return unless update_issue_from_params
178 174 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
179 175 saved = false
180 176 begin
181 177 saved = @issue.save_issue_with_child_records(params, @time_entry)
182 178 rescue ActiveRecord::StaleObjectError
183 179 @conflict = true
184 180 if params[:last_journal_id]
185 181 if params[:last_journal_id].present?
186 182 last_journal_id = params[:last_journal_id].to_i
187 183 @conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
188 184 else
189 185 @conflict_journals = @issue.journals.all
190 186 end
191 187 end
192 188 end
193 189
194 190 if saved
195 191 render_attachment_warning_if_needed(@issue)
196 192 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
197 193
198 194 respond_to do |format|
199 195 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
200 196 format.api { render_api_ok }
201 197 end
202 198 else
203 199 respond_to do |format|
204 200 format.html { render :action => 'edit' }
205 201 format.api { render_validation_errors(@issue) }
206 202 end
207 203 end
208 204 end
209 205
210 206 # Bulk edit/copy a set of issues
211 207 def bulk_edit
212 208 @issues.sort!
213 209 @copy = params[:copy].present?
214 210 @notes = params[:notes]
215 211
216 212 if User.current.allowed_to?(:move_issues, @projects)
217 213 @allowed_projects = Issue.allowed_target_projects_on_move
218 214 if params[:issue]
219 215 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
220 216 if @target_project
221 217 target_projects = [@target_project]
222 218 end
223 219 end
224 220 end
225 221 target_projects ||= @projects
226 222
227 223 if @copy
228 224 @available_statuses = [IssueStatus.default]
229 225 else
230 226 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
231 227 end
232 228 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
233 229 @assignables = target_projects.map(&:assignable_users).reduce(:&)
234 230 @trackers = target_projects.map(&:trackers).reduce(:&)
235 231 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
236 232 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
237 233 if @copy
238 234 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
239 235 end
240 236
241 237 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
242 238 render :layout => false if request.xhr?
243 239 end
244 240
245 241 def bulk_update
246 242 @issues.sort!
247 243 @copy = params[:copy].present?
248 244 attributes = parse_params_for_bulk_issue_attributes(params)
249 245
250 246 unsaved_issue_ids = []
251 247 moved_issues = []
252 248 @issues.each do |issue|
253 249 issue.reload
254 250 if @copy
255 251 issue = issue.copy({}, :attachments => params[:copy_attachments].present?)
256 252 end
257 253 journal = issue.init_journal(User.current, params[:notes])
258 254 issue.safe_attributes = attributes
259 255 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
260 256 if issue.save
261 257 moved_issues << issue
262 258 else
263 259 # Keep unsaved issue ids to display them in flash error
264 260 unsaved_issue_ids << issue.id
265 261 end
266 262 end
267 263 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
268 264
269 265 if params[:follow]
270 266 if @issues.size == 1 && moved_issues.size == 1
271 267 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
272 268 elsif moved_issues.map(&:project).uniq.size == 1
273 269 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
274 270 end
275 271 else
276 272 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
277 273 end
278 274 end
279 275
280 276 def destroy
281 277 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
282 278 if @hours > 0
283 279 case params[:todo]
284 280 when 'destroy'
285 281 # nothing to do
286 282 when 'nullify'
287 283 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
288 284 when 'reassign'
289 285 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
290 286 if reassign_to.nil?
291 287 flash.now[:error] = l(:error_issue_not_found_in_project)
292 288 return
293 289 else
294 290 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
295 291 end
296 292 else
297 293 # display the destroy form if it's a user request
298 294 return unless api_request?
299 295 end
300 296 end
301 297 @issues.each do |issue|
302 298 begin
303 299 issue.reload.destroy
304 300 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
305 301 # nothing to do, issue was already deleted (eg. by a parent)
306 302 end
307 303 end
308 304 respond_to do |format|
309 305 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
310 306 format.api { render_api_ok }
311 307 end
312 308 end
313 309
314 310 private
315 311 def find_issue
316 312 # Issue.visible.find(...) can not be used to redirect user to the login form
317 313 # if the issue actually exists but requires authentication
318 314 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
319 315 unless @issue.visible?
320 316 deny_access
321 317 return
322 318 end
323 319 @project = @issue.project
324 320 rescue ActiveRecord::RecordNotFound
325 321 render_404
326 322 end
327 323
328 324 def find_project
329 325 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
330 326 @project = Project.find(project_id)
331 327 rescue ActiveRecord::RecordNotFound
332 328 render_404
333 329 end
334 330
335 331 def retrieve_previous_and_next_issue_ids
336 332 retrieve_query_from_session
337 333 if @query
338 334 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
339 335 sort_update(@query.sortable_columns, 'issues_index_sort')
340 336 limit = 500
341 337 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
342 338 if (idx = issue_ids.index(@issue.id)) && idx < limit
343 339 if issue_ids.size < 500
344 340 @issue_position = idx + 1
345 341 @issue_count = issue_ids.size
346 342 end
347 343 @prev_issue_id = issue_ids[idx - 1] if idx > 0
348 344 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
349 345 end
350 346 end
351 347 end
352 348
353 349 # Used by #edit and #update to set some common instance variables
354 350 # from the params
355 351 # TODO: Refactor, not everything in here is needed by #edit
356 352 def update_issue_from_params
357 353 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
358 354 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
359 355 @time_entry.attributes = params[:time_entry]
360 356
361 357 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
362 358 @issue.init_journal(User.current, @notes)
363 359
364 360 issue_attributes = params[:issue]
365 361 if issue_attributes && params[:conflict_resolution]
366 362 case params[:conflict_resolution]
367 363 when 'overwrite'
368 364 issue_attributes = issue_attributes.dup
369 365 issue_attributes.delete(:lock_version)
370 366 when 'add_notes'
371 367 issue_attributes = {}
372 368 when 'cancel'
373 369 redirect_to issue_path(@issue)
374 370 return false
375 371 end
376 372 end
377 373 @issue.safe_attributes = issue_attributes
378 374 @priorities = IssuePriority.active
379 375 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
380 376 true
381 377 end
382 378
383 379 # TODO: Refactor, lots of extra code in here
384 380 # TODO: Changing tracker on an existing issue should not trigger this
385 381 def build_new_issue_from_params
386 382 if params[:id].blank?
387 383 @issue = Issue.new
388 384 if params[:copy_from]
389 385 begin
390 386 @copy_from = Issue.visible.find(params[:copy_from])
391 387 @copy_attachments = params[:copy_attachments].present? || request.get?
392 388 @issue.copy_from(@copy_from, :attachments => @copy_attachments)
393 389 rescue ActiveRecord::RecordNotFound
394 390 render_404
395 391 return
396 392 end
397 393 end
398 394 @issue.project = @project
399 395 else
400 396 @issue = @project.issues.visible.find(params[:id])
401 397 end
402 398
403 399 @issue.project = @project
404 400 @issue.author = User.current
405 401 # Tracker must be set before custom field values
406 402 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
407 403 if @issue.tracker.nil?
408 404 render_error l(:error_no_tracker_in_project)
409 405 return false
410 406 end
411 407 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
412 408 @issue.safe_attributes = params[:issue]
413 409
414 410 @priorities = IssuePriority.active
415 411 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
416 412 @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
417 413 end
418 414
419 415 def check_for_default_issue_status
420 416 if IssueStatus.default.nil?
421 417 render_error l(:error_no_default_issue_status)
422 418 return false
423 419 end
424 420 end
425 421
426 422 def parse_params_for_bulk_issue_attributes(params)
427 423 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
428 424 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
429 425 if custom = attributes[:custom_field_values]
430 426 custom.reject! {|k,v| v.blank?}
431 427 custom.keys.each do |k|
432 428 if custom[k].is_a?(Array)
433 429 custom[k] << '' if custom[k].delete('__none__')
434 430 else
435 431 custom[k] = '' if custom[k] == '__none__'
436 432 end
437 433 end
438 434 end
439 435 attributes
440 436 end
441 437 end
@@ -1,99 +1,99
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 RolesController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :index
22 22 before_filter :require_admin_or_api_request, :only => :index
23 23 before_filter :find_role, :only => [:edit, :update, :destroy]
24 24 accept_api_auth :index
25 25
26 26 def index
27 27 respond_to do |format|
28 28 format.html {
29 29 @role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position'
30 30 render :action => "index", :layout => false if request.xhr?
31 31 }
32 32 format.api {
33 33 @roles = Role.givable.all
34 34 }
35 35 end
36 36 end
37 37
38 38 def new
39 39 # Prefills the form with 'Non member' role permissions
40 40 @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
41 41 @roles = Role.sorted.all
42 42 end
43 43
44 44 def create
45 45 @role = Role.new(params[:role])
46 46 if request.post? && @role.save
47 47 # workflow copy
48 48 if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
49 @role.workflows.copy(copy_from)
49 @role.workflow_rules.copy(copy_from)
50 50 end
51 51 flash[:notice] = l(:notice_successful_create)
52 52 redirect_to :action => 'index'
53 53 else
54 54 @roles = Role.sorted.all
55 55 render :action => 'new'
56 56 end
57 57 end
58 58
59 59 def edit
60 60 end
61 61
62 62 def update
63 63 if request.put? and @role.update_attributes(params[:role])
64 64 flash[:notice] = l(:notice_successful_update)
65 65 redirect_to :action => 'index'
66 66 else
67 67 render :action => 'edit'
68 68 end
69 69 end
70 70
71 71 def destroy
72 72 @role.destroy
73 73 redirect_to :action => 'index'
74 74 rescue
75 75 flash[:error] = l(:error_can_not_remove_role)
76 76 redirect_to :action => 'index'
77 77 end
78 78
79 79 def permissions
80 80 @roles = Role.sorted.all
81 81 @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
82 82 if request.post?
83 83 @roles.each do |role|
84 84 role.permissions = params[:permissions][role.id.to_s]
85 85 role.save
86 86 end
87 87 flash[:notice] = l(:notice_successful_update)
88 88 redirect_to :action => 'index'
89 89 end
90 90 end
91 91
92 92 private
93 93
94 94 def find_role
95 95 @role = Role.find(params[:id])
96 96 rescue ActiveRecord::RecordNotFound
97 97 render_404
98 98 end
99 99 end
@@ -1,83 +1,83
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 TrackersController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :index
22 22 before_filter :require_admin_or_api_request, :only => :index
23 23 accept_api_auth :index
24 24
25 25 def index
26 26 respond_to do |format|
27 27 format.html {
28 28 @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
29 29 render :action => "index", :layout => false if request.xhr?
30 30 }
31 31 format.api {
32 32 @trackers = Tracker.sorted.all
33 33 }
34 34 end
35 35 end
36 36
37 37 def new
38 38 @tracker ||= Tracker.new(params[:tracker])
39 39 @trackers = Tracker.find :all, :order => 'position'
40 40 @projects = Project.find(:all)
41 41 end
42 42
43 43 def create
44 44 @tracker = Tracker.new(params[:tracker])
45 45 if request.post? and @tracker.save
46 46 # workflow copy
47 47 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
48 @tracker.workflows.copy(copy_from)
48 @tracker.workflow_rules.copy(copy_from)
49 49 end
50 50 flash[:notice] = l(:notice_successful_create)
51 51 redirect_to :action => 'index'
52 52 return
53 53 end
54 54 new
55 55 render :action => 'new'
56 56 end
57 57
58 58 def edit
59 59 @tracker ||= Tracker.find(params[:id])
60 60 @projects = Project.find(:all)
61 61 end
62 62
63 63 def update
64 64 @tracker = Tracker.find(params[:id])
65 65 if request.put? and @tracker.update_attributes(params[:tracker])
66 66 flash[:notice] = l(:notice_successful_update)
67 67 redirect_to :action => 'index'
68 68 return
69 69 end
70 70 edit
71 71 render :action => 'edit'
72 72 end
73 73
74 74 def destroy
75 75 @tracker = Tracker.find(params[:id])
76 76 unless @tracker.issues.empty?
77 77 flash[:error] = l(:error_can_not_delete_tracker)
78 78 else
79 79 @tracker.destroy
80 80 end
81 81 redirect_to :action => 'index'
82 82 end
83 83 end
@@ -1,102 +1,132
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class WorkflowsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22 before_filter :find_roles
23 23 before_filter :find_trackers
24 24
25 25 def index
26 @workflow_counts = Workflow.count_by_tracker_and_role
26 @workflow_counts = WorkflowTransition.count_by_tracker_and_role
27 27 end
28 28
29 29 def edit
30 30 @role = Role.find_by_id(params[:role_id])
31 31 @tracker = Tracker.find_by_id(params[:tracker_id])
32 32
33 33 if request.post?
34 Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
34 WorkflowTransition.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
35 35 (params[:issue_status] || []).each { |status_id, transitions|
36 36 transitions.each { |new_status_id, options|
37 37 author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
38 38 assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
39 @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
39 WorkflowTransition.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
40 40 }
41 41 }
42 42 if @role.save
43 flash[:notice] = l(:notice_successful_update)
44 43 redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
45 44 return
46 45 end
47 46 end
48 47
49 48 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
50 49 if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
51 50 @statuses = @tracker.issue_statuses
52 51 end
53 52 @statuses ||= IssueStatus.find(:all, :order => 'position')
54 53
55 54 if @tracker && @role && @statuses.any?
56 workflows = Workflow.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id})
55 workflows = WorkflowTransition.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id})
57 56 @workflows = {}
58 57 @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
59 58 @workflows['author'] = workflows.select {|w| w.author}
60 59 @workflows['assignee'] = workflows.select {|w| w.assignee}
61 60 end
62 61 end
63 62
63 def permissions
64 @role = Role.find_by_id(params[:role_id])
65 @tracker = Tracker.find_by_id(params[:tracker_id])
66
67 if @role && @tracker
68 if request.post?
69 WorkflowPermission.destroy_all({:role_id => @role.id, :tracker_id => @tracker.id})
70 (params[:permissions] || {}).each { |field, rule_by_status_id|
71 rule_by_status_id.each { |status_id, rule|
72 if rule.present?
73 WorkflowPermission.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule)
74 end
75 }
76 }
77 redirect_to :action => 'permissions', :role_id => @role, :tracker_id => @tracker
78 return
79 end
80
81 @statuses = @tracker.issue_statuses
82 @fields = (Tracker::CORE_FIELDS_ALL - @tracker.disabled_core_fields).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
83 @custom_fields = @tracker.custom_fields
84
85 @permissions = WorkflowPermission.where(:tracker_id => @tracker.id, :role_id => @role.id).all.inject({}) do |h, w|
86 h[w.old_status_id] ||= {}
87 h[w.old_status_id][w.field_name] = w.rule
88 h
89 end
90 @statuses.each {|status| @permissions[status.id] ||= {}}
91 end
92 end
93
64 94 def copy
65 95
66 96 if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
67 97 @source_tracker = nil
68 98 else
69 99 @source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
70 100 end
71 101 if params[:source_role_id].blank? || params[:source_role_id] == 'any'
72 102 @source_role = nil
73 103 else
74 104 @source_role = Role.find_by_id(params[:source_role_id].to_i)
75 105 end
76 106
77 107 @target_trackers = params[:target_tracker_ids].blank? ? nil : Tracker.find_all_by_id(params[:target_tracker_ids])
78 108 @target_roles = params[:target_role_ids].blank? ? nil : Role.find_all_by_id(params[:target_role_ids])
79 109
80 110 if request.post?
81 111 if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
82 112 flash.now[:error] = l(:error_workflow_copy_source)
83 113 elsif @target_trackers.nil? || @target_roles.nil?
84 114 flash.now[:error] = l(:error_workflow_copy_target)
85 115 else
86 Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
116 WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
87 117 flash[:notice] = l(:notice_successful_update)
88 118 redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role
89 119 end
90 120 end
91 121 end
92 122
93 123 private
94 124
95 125 def find_roles
96 126 @roles = Role.find(:all, :order => 'builtin, position')
97 127 end
98 128
99 129 def find_trackers
100 130 @trackers = Tracker.find(:all, :order => 'position')
101 131 end
102 132 end
@@ -1,156 +1,158
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2012 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 CustomFieldsHelper
21 21
22 22 def custom_fields_tabs
23 23 tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
24 24 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
25 25 {:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
26 26 {:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
27 27 {:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
28 28 {:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
29 29 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
30 30 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
31 31 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
32 32 ]
33 33 end
34 34
35 35 # Return custom field html tag corresponding to its format
36 36 def custom_field_tag(name, custom_value)
37 37 custom_field = custom_value.custom_field
38 38 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
39 39 field_name << "[]" if custom_field.multiple?
40 40 field_id = "#{name}_custom_field_values_#{custom_field.id}"
41 41
42 42 tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
43 43
44 44 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
45 45 case field_format.try(:edit_as)
46 46 when "date"
47 47 text_field_tag(field_name, custom_value.value, tag_options.merge(:size => 10)) +
48 48 calendar_for(field_id)
49 49 when "text"
50 50 text_area_tag(field_name, custom_value.value, tag_options.merge(:rows => 3))
51 51 when "bool"
52 52 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, tag_options)
53 53 when "list"
54 54 blank_option = ''.html_safe
55 55 unless custom_field.multiple?
56 56 if custom_field.is_required?
57 57 unless custom_field.default_value.present?
58 58 blank_option = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
59 59 end
60 60 else
61 61 blank_option = content_tag('option')
62 62 end
63 63 end
64 64 s = select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value),
65 65 tag_options.merge(:multiple => custom_field.multiple?))
66 66 if custom_field.multiple?
67 67 s << hidden_field_tag(field_name, '')
68 68 end
69 69 s
70 70 else
71 71 text_field_tag(field_name, custom_value.value, tag_options)
72 72 end
73 73 end
74 74
75 75 # Return custom field label tag
76 def custom_field_label_tag(name, custom_value)
76 def custom_field_label_tag(name, custom_value, options={})
77 required = options[:required] || custom_value.custom_field.is_required?
78
77 79 content_tag "label", h(custom_value.custom_field.name) +
78 (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>".html_safe : ""),
79 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
80 (required ? " <span class=\"required\">*</span>".html_safe : ""),
81 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
80 82 end
81 83
82 84 # Return custom field tag with its label tag
83 def custom_field_tag_with_label(name, custom_value)
84 custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
85 def custom_field_tag_with_label(name, custom_value, options={})
86 custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
85 87 end
86 88
87 89 def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil)
88 90 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
89 91 field_name << "[]" if custom_field.multiple?
90 92 field_id = "#{name}_custom_field_values_#{custom_field.id}"
91 93
92 94 tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
93 95
94 96 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
95 97 case field_format.try(:edit_as)
96 98 when "date"
97 99 text_field_tag(field_name, '', tag_options.merge(:size => 10)) +
98 100 calendar_for(field_id)
99 101 when "text"
100 102 text_area_tag(field_name, '', tag_options.merge(:rows => 3))
101 103 when "bool"
102 104 select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
103 105 [l(:general_text_yes), '1'],
104 106 [l(:general_text_no), '0']]), tag_options)
105 107 when "list"
106 108 options = []
107 109 options << [l(:label_no_change_option), ''] unless custom_field.multiple?
108 110 options << [l(:label_none), '__none__'] unless custom_field.is_required?
109 111 options += custom_field.possible_values_options(projects)
110 112 select_tag(field_name, options_for_select(options), tag_options.merge(:multiple => custom_field.multiple?))
111 113 else
112 114 text_field_tag(field_name, '', tag_options)
113 115 end
114 116 end
115 117
116 118 # Return a string used to display a custom value
117 119 def show_value(custom_value)
118 120 return "" unless custom_value
119 121 format_value(custom_value.value, custom_value.custom_field.field_format)
120 122 end
121 123
122 124 # Return a string used to display a custom value
123 125 def format_value(value, field_format)
124 126 if value.is_a?(Array)
125 127 value.collect {|v| format_value(v, field_format)}.compact.sort.join(', ')
126 128 else
127 129 Redmine::CustomFieldFormat.format_value(value, field_format)
128 130 end
129 131 end
130 132
131 133 # Return an array of custom field formats which can be used in select_tag
132 134 def custom_field_formats_for_select(custom_field)
133 135 Redmine::CustomFieldFormat.as_select(custom_field.class.customized_class.name)
134 136 end
135 137
136 138 # Renders the custom_values in api views
137 139 def render_api_custom_values(custom_values, api)
138 140 api.array :custom_fields do
139 141 custom_values.each do |custom_value|
140 142 attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
141 143 attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
142 144 api.custom_field attrs do
143 145 if custom_value.value.is_a?(Array)
144 146 api.array :value do
145 147 custom_value.value.each do |value|
146 148 api.value value unless value.blank?
147 149 end
148 150 end
149 151 else
150 152 api.value custom_value.value
151 153 end
152 154 end
153 155 end
154 156 end unless custom_values.empty?
155 157 end
156 158 end
@@ -1,21 +1,27
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2012 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 def field_permission_tag(permissions, status, field)
22 name = field.is_a?(CustomField) ? field.id.to_s : field
23 select_tag("permissions[#{name}][#{status.id}]",
24 options_for_select([["", ""], ["Read-only", "readonly"], ["Required", "required"]], permissions[status.id][name])
25 )
26 end
21 27 end
@@ -1,1117 +1,1223
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 belongs_to :project
22 22 belongs_to :tracker
23 23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29 29
30 30 has_many :journals, :as => :journalized, :dependent => :destroy
31 31 has_many :time_entries, :dependent => :delete_all
32 32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33 33
34 34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 36
37 37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 39 acts_as_customizable
40 40 acts_as_watchable
41 41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 42 :include => [:project, :journals],
43 43 # sort by id so that limited eager loading doesn't break with postgresql
44 44 :order_column => "#{table_name}.id"
45 45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48 48
49 49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 50 :author_key => :author_id
51 51
52 52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53 53
54 54 attr_reader :current_journal
55 55
56 56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57 57
58 58 validates_length_of :subject, :maximum => 255
59 59 validates_inclusion_of :done_ratio, :in => 0..100
60 60 validates_numericality_of :estimated_hours, :allow_nil => true
61 validate :validate_issue
61 validate :validate_issue, :validate_required_fields
62 62
63 63 scope :visible,
64 64 lambda {|*args| { :include => :project,
65 65 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
66 66
67 67 scope :open, lambda {|*args|
68 68 is_closed = args.size > 0 ? !args.first : false
69 69 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
70 70 }
71 71
72 72 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
73 73 scope :with_limit, lambda { |limit| { :limit => limit} }
74 74 scope :on_active_project, :include => [:status, :project, :tracker],
75 75 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
76 76
77 77 before_create :default_assign
78 78 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
79 79 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
80 80 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
81 81 after_destroy :update_parent_attributes
82 82
83 83 # Returns a SQL conditions string used to find all issues visible by the specified user
84 84 def self.visible_condition(user, options={})
85 85 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
86 86 case role.issues_visibility
87 87 when 'all'
88 88 nil
89 89 when 'default'
90 90 user_ids = [user.id] + user.groups.map(&:id)
91 91 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
92 92 when 'own'
93 93 user_ids = [user.id] + user.groups.map(&:id)
94 94 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
95 95 else
96 96 '1=0'
97 97 end
98 98 end
99 99 end
100 100
101 101 # Returns true if usr or current user is allowed to view the issue
102 102 def visible?(usr=nil)
103 103 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
104 104 case role.issues_visibility
105 105 when 'all'
106 106 true
107 107 when 'default'
108 108 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
109 109 when 'own'
110 110 self.author == user || user.is_or_belongs_to?(assigned_to)
111 111 else
112 112 false
113 113 end
114 114 end
115 115 end
116 116
117 117 def initialize(attributes=nil, *args)
118 118 super
119 119 if new_record?
120 120 # set default values for new records only
121 121 self.status ||= IssueStatus.default
122 122 self.priority ||= IssuePriority.default
123 123 self.watcher_user_ids = []
124 124 end
125 125 end
126 126
127 127 # AR#Persistence#destroy would raise and RecordNotFound exception
128 128 # if the issue was already deleted or updated (non matching lock_version).
129 129 # This is a problem when bulk deleting issues or deleting a project
130 130 # (because an issue may already be deleted if its parent was deleted
131 131 # first).
132 132 # The issue is reloaded by the nested_set before being deleted so
133 133 # the lock_version condition should not be an issue but we handle it.
134 134 def destroy
135 135 super
136 136 rescue ActiveRecord::RecordNotFound
137 137 # Stale or already deleted
138 138 begin
139 139 reload
140 140 rescue ActiveRecord::RecordNotFound
141 141 # The issue was actually already deleted
142 142 @destroyed = true
143 143 return freeze
144 144 end
145 145 # The issue was stale, retry to destroy
146 146 super
147 147 end
148 148
149 def reload(*args)
150 @workflow_rule_by_attribute = nil
151 super
152 end
153
149 154 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
150 155 def available_custom_fields
151 156 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
152 157 end
153 158
154 159 # Copies attributes from another issue, arg can be an id or an Issue
155 160 def copy_from(arg, options={})
156 161 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
157 162 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
158 163 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
159 164 self.status = issue.status
160 165 self.author = User.current
161 166 unless options[:attachments] == false
162 167 self.attachments = issue.attachments.map do |attachement|
163 168 attachement.copy(:container => self)
164 169 end
165 170 end
166 171 @copied_from = issue
167 172 self
168 173 end
169 174
170 175 # Returns an unsaved copy of the issue
171 176 def copy(attributes=nil, copy_options={})
172 177 copy = self.class.new.copy_from(self, copy_options)
173 178 copy.attributes = attributes if attributes
174 179 copy
175 180 end
176 181
177 182 # Returns true if the issue is a copy
178 183 def copy?
179 184 @copied_from.present?
180 185 end
181 186
182 187 # Moves/copies an issue to a new project and tracker
183 188 # Returns the moved/copied issue on success, false on failure
184 189 def move_to_project(new_project, new_tracker=nil, options={})
185 190 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
186 191
187 192 if options[:copy]
188 193 issue = self.copy
189 194 else
190 195 issue = self
191 196 end
192 197
193 198 issue.init_journal(User.current, options[:notes])
194 199
195 200 # Preserve previous behaviour
196 201 # #move_to_project doesn't change tracker automatically
197 202 issue.send :project=, new_project, true
198 203 if new_tracker
199 204 issue.tracker = new_tracker
200 205 end
201 206 # Allow bulk setting of attributes on the issue
202 207 if options[:attributes]
203 208 issue.attributes = options[:attributes]
204 209 end
205 210
206 211 issue.save ? issue : false
207 212 end
208 213
209 214 def status_id=(sid)
210 215 self.status = nil
211 write_attribute(:status_id, sid)
216 result = write_attribute(:status_id, sid)
217 @workflow_rule_by_attribute = nil
218 result
212 219 end
213 220
214 221 def priority_id=(pid)
215 222 self.priority = nil
216 223 write_attribute(:priority_id, pid)
217 224 end
218 225
219 226 def category_id=(cid)
220 227 self.category = nil
221 228 write_attribute(:category_id, cid)
222 229 end
223 230
224 231 def fixed_version_id=(vid)
225 232 self.fixed_version = nil
226 233 write_attribute(:fixed_version_id, vid)
227 234 end
228 235
229 236 def tracker_id=(tid)
230 237 self.tracker = nil
231 238 result = write_attribute(:tracker_id, tid)
232 239 @custom_field_values = nil
240 @workflow_rule_by_attribute = nil
233 241 result
234 242 end
235 243
236 244 def project_id=(project_id)
237 245 if project_id.to_s != self.project_id.to_s
238 246 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
239 247 end
240 248 end
241 249
242 250 def project=(project, keep_tracker=false)
243 251 project_was = self.project
244 252 write_attribute(:project_id, project ? project.id : nil)
245 253 association_instance_set('project', project)
246 254 if project_was && project && project_was != project
247 255 unless keep_tracker || project.trackers.include?(tracker)
248 256 self.tracker = project.trackers.first
249 257 end
250 258 # Reassign to the category with same name if any
251 259 if category
252 260 self.category = project.issue_categories.find_by_name(category.name)
253 261 end
254 262 # Keep the fixed_version if it's still valid in the new_project
255 263 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
256 264 self.fixed_version = nil
257 265 end
258 266 if parent && parent.project_id != project_id
259 267 self.parent_issue_id = nil
260 268 end
261 269 @custom_field_values = nil
262 270 end
263 271 end
264 272
265 273 def description=(arg)
266 274 if arg.is_a?(String)
267 275 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
268 276 end
269 277 write_attribute(:description, arg)
270 278 end
271 279
272 280 # Overrides assign_attributes so that project and tracker get assigned first
273 281 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
274 282 return if new_attributes.nil?
275 283 attrs = new_attributes.dup
276 284 attrs.stringify_keys!
277 285
278 286 %w(project project_id tracker tracker_id).each do |attr|
279 287 if attrs.has_key?(attr)
280 288 send "#{attr}=", attrs.delete(attr)
281 289 end
282 290 end
283 291 send :assign_attributes_without_project_and_tracker_first, attrs, *args
284 292 end
285 293 # Do not redefine alias chain on reload (see #4838)
286 294 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
287 295
288 296 def estimated_hours=(h)
289 297 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
290 298 end
291 299
292 300 safe_attributes 'project_id',
293 301 :if => lambda {|issue, user|
294 302 if issue.new_record?
295 303 issue.copy?
296 304 elsif user.allowed_to?(:move_issues, issue.project)
297 305 projects = Issue.allowed_target_projects_on_move(user)
298 306 projects.include?(issue.project) && projects.size > 1
299 307 end
300 308 }
301 309
302 310 safe_attributes 'tracker_id',
303 311 'status_id',
304 312 'category_id',
305 313 'assigned_to_id',
306 314 'priority_id',
307 315 'fixed_version_id',
308 316 'subject',
309 317 'description',
310 318 'start_date',
311 319 'due_date',
312 320 'done_ratio',
313 321 'estimated_hours',
314 322 'custom_field_values',
315 323 'custom_fields',
316 324 'lock_version',
317 325 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
318 326
319 327 safe_attributes 'status_id',
320 328 'assigned_to_id',
321 329 'fixed_version_id',
322 330 'done_ratio',
323 331 'lock_version',
324 332 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
325 333
326 334 safe_attributes 'watcher_user_ids',
327 335 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
328 336
329 337 safe_attributes 'is_private',
330 338 :if => lambda {|issue, user|
331 339 user.allowed_to?(:set_issues_private, issue.project) ||
332 340 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
333 341 }
334 342
335 343 safe_attributes 'parent_issue_id',
336 344 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
337 345 user.allowed_to?(:manage_subtasks, issue.project)}
338 346
339 def safe_attribute_names(*args)
340 names = super(*args)
347 def safe_attribute_names(user=nil)
348 names = super
341 349 names -= disabled_core_fields
350 names -= read_only_attribute_names(user)
342 351 names
343 352 end
344 353
345 354 # Safely sets attributes
346 355 # Should be called from controllers instead of #attributes=
347 356 # attr_accessible is too rough because we still want things like
348 357 # Issue.new(:project => foo) to work
349 358 def safe_attributes=(attrs, user=User.current)
350 359 return unless attrs.is_a?(Hash)
351 360
352 361 attrs = attrs.dup
353 362
354 363 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
355 364 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
356 365 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
357 366 self.project_id = p
358 367 end
359 368 end
360 369
361 370 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
362 371 self.tracker_id = t
363 372 end
364 373
365 attrs = delete_unsafe_attributes(attrs, user)
366 return if attrs.empty?
367
368 if attrs['status_id']
369 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
370 attrs.delete('status_id')
374 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
375 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
376 self.status_id = s
371 377 end
372 378 end
373 379
380 attrs = delete_unsafe_attributes(attrs, user)
381 return if attrs.empty?
382
374 383 unless leaf?
375 384 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
376 385 end
377 386
378 387 if attrs['parent_issue_id'].present?
379 388 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
380 389 end
381 390
391 if attrs['custom_field_values'].present?
392 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
393 end
394
395 if attrs['custom_fields'].present?
396 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
397 end
398
382 399 # mass-assignment security bypass
383 400 assign_attributes attrs, :without_protection => true
384 401 end
385 402
386 403 def disabled_core_fields
387 404 tracker ? tracker.disabled_core_fields : []
388 405 end
389 406
407 # Returns the custom_field_values that can be edited by the given user
408 def editable_custom_field_values(user=nil)
409 custom_field_values.reject do |value|
410 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
411 end
412 end
413
414 # Returns the names of attributes that are read-only for user or the current user
415 # For users with multiple roles, the read-only fields are the intersection of
416 # read-only fields of each role
417 # The result is an array of strings where sustom fields are represented with their ids
418 #
419 # Examples:
420 # issue.read_only_attribute_names # => ['due_date', '2']
421 # issue.read_only_attribute_names(user) # => []
422 def read_only_attribute_names(user=nil)
423 workflow_rule_by_attribute(user).select {|attr, rule| rule == 'readonly'}.keys
424 end
425
426 # Returns the names of required attributes for user or the current user
427 # For users with multiple roles, the required fields are the intersection of
428 # required fields of each role
429 # The result is an array of strings where sustom fields are represented with their ids
430 #
431 # Examples:
432 # issue.required_attribute_names # => ['due_date', '2']
433 # issue.required_attribute_names(user) # => []
434 def required_attribute_names(user=nil)
435 workflow_rule_by_attribute(user).select {|attr, rule| rule == 'required'}.keys
436 end
437
438 # Returns true if the attribute is required for user
439 def required_attribute?(name, user=nil)
440 required_attribute_names(user).include?(name.to_s)
441 end
442
443 # Returns a hash of the workflow rule by attribute for the given user
444 #
445 # Examples:
446 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
447 def workflow_rule_by_attribute(user=nil)
448 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
449
450 user_real = user || User.current
451 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
452 return {} if roles.empty?
453
454 result = {}
455 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
456 if workflow_permissions.any?
457 workflow_rules = workflow_permissions.inject({}) do |h, wp|
458 h[wp.field_name] ||= []
459 h[wp.field_name] << wp.rule
460 h
461 end
462 workflow_rules.each do |attr, rules|
463 next if rules.size < roles.size
464 uniq_rules = rules.uniq
465 if uniq_rules.size == 1
466 result[attr] = uniq_rules.first
467 else
468 result[attr] = 'required'
469 end
470 end
471 end
472 @workflow_rule_by_attribute = result if user.nil?
473 result
474 end
475 private :workflow_rule_by_attribute
476
390 477 def done_ratio
391 478 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
392 479 status.default_done_ratio
393 480 else
394 481 read_attribute(:done_ratio)
395 482 end
396 483 end
397 484
398 485 def self.use_status_for_done_ratio?
399 486 Setting.issue_done_ratio == 'issue_status'
400 487 end
401 488
402 489 def self.use_field_for_done_ratio?
403 490 Setting.issue_done_ratio == 'issue_field'
404 491 end
405 492
406 493 def validate_issue
407 494 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
408 495 errors.add :due_date, :not_a_date
409 496 end
410 497
411 498 if self.due_date and self.start_date and self.due_date < self.start_date
412 499 errors.add :due_date, :greater_than_start_date
413 500 end
414 501
415 502 if start_date && soonest_start && start_date < soonest_start
416 503 errors.add :start_date, :invalid
417 504 end
418 505
419 506 if fixed_version
420 507 if !assignable_versions.include?(fixed_version)
421 508 errors.add :fixed_version_id, :inclusion
422 509 elsif reopened? && fixed_version.closed?
423 510 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
424 511 end
425 512 end
426 513
427 514 # Checks that the issue can not be added/moved to a disabled tracker
428 515 if project && (tracker_id_changed? || project_id_changed?)
429 516 unless project.trackers.include?(tracker)
430 517 errors.add :tracker_id, :inclusion
431 518 end
432 519 end
433 520
434 521 # Checks parent issue assignment
435 522 if @parent_issue
436 523 if @parent_issue.project_id != project_id
437 524 errors.add :parent_issue_id, :not_same_project
438 525 elsif !new_record?
439 526 # moving an existing issue
440 527 if @parent_issue.root_id != root_id
441 528 # we can always move to another tree
442 529 elsif move_possible?(@parent_issue)
443 530 # move accepted inside tree
444 531 else
445 532 errors.add :parent_issue_id, :not_a_valid_parent
446 533 end
447 534 end
448 535 end
449 536 end
450 537
538 # Validates the issue against additional workflow requirements
539 def validate_required_fields
540 user = new_record? ? author : current_journal.try(:user)
541
542 required_attribute_names(user).each do |attribute|
543 if attribute =~ /^\d+$/
544 attribute = attribute.to_i
545 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
546 if v && v.value.blank?
547 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
548 end
549 else
550 if respond_to?(attribute) && send(attribute).blank?
551 errors.add attribute, :blank
552 end
553 end
554 end
555 end
556
451 557 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
452 558 # even if the user turns off the setting later
453 559 def update_done_ratio_from_issue_status
454 560 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
455 561 self.done_ratio = status.default_done_ratio
456 562 end
457 563 end
458 564
459 565 def init_journal(user, notes = "")
460 566 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
461 567 if new_record?
462 568 @current_journal.notify = false
463 569 else
464 570 @attributes_before_change = attributes.dup
465 571 @custom_values_before_change = {}
466 572 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
467 573 end
468 574 @current_journal
469 575 end
470 576
471 577 # Returns the id of the last journal or nil
472 578 def last_journal_id
473 579 if new_record?
474 580 nil
475 581 else
476 582 journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
477 583 end
478 584 end
479 585
480 586 # Return true if the issue is closed, otherwise false
481 587 def closed?
482 588 self.status.is_closed?
483 589 end
484 590
485 591 # Return true if the issue is being reopened
486 592 def reopened?
487 593 if !new_record? && status_id_changed?
488 594 status_was = IssueStatus.find_by_id(status_id_was)
489 595 status_new = IssueStatus.find_by_id(status_id)
490 596 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
491 597 return true
492 598 end
493 599 end
494 600 false
495 601 end
496 602
497 603 # Return true if the issue is being closed
498 604 def closing?
499 605 if !new_record? && status_id_changed?
500 606 status_was = IssueStatus.find_by_id(status_id_was)
501 607 status_new = IssueStatus.find_by_id(status_id)
502 608 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
503 609 return true
504 610 end
505 611 end
506 612 false
507 613 end
508 614
509 615 # Returns true if the issue is overdue
510 616 def overdue?
511 617 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
512 618 end
513 619
514 620 # Is the amount of work done less than it should for the due date
515 621 def behind_schedule?
516 622 return false if start_date.nil? || due_date.nil?
517 623 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
518 624 return done_date <= Date.today
519 625 end
520 626
521 627 # Does this issue have children?
522 628 def children?
523 629 !leaf?
524 630 end
525 631
526 632 # Users the issue can be assigned to
527 633 def assignable_users
528 634 users = project.assignable_users
529 635 users << author if author
530 636 users << assigned_to if assigned_to
531 637 users.uniq.sort
532 638 end
533 639
534 640 # Versions that the issue can be assigned to
535 641 def assignable_versions
536 642 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
537 643 end
538 644
539 645 # Returns true if this issue is blocked by another issue that is still open
540 646 def blocked?
541 647 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
542 648 end
543 649
544 650 # Returns an array of statuses that user is able to apply
545 651 def new_statuses_allowed_to(user=User.current, include_default=false)
546 652 if new_record? && @copied_from
547 653 [IssueStatus.default, @copied_from.status].compact.uniq.sort
548 654 else
549 655 initial_status = nil
550 656 if new_record?
551 657 initial_status = IssueStatus.default
552 658 elsif status_id_was
553 659 initial_status = IssueStatus.find_by_id(status_id_was)
554 660 end
555 661 initial_status ||= status
556 662
557 663 statuses = initial_status.find_new_statuses_allowed_to(
558 664 user.admin ? Role.all : user.roles_for_project(project),
559 665 tracker,
560 666 author == user,
561 667 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
562 668 )
563 669 statuses << initial_status unless statuses.empty?
564 670 statuses << IssueStatus.default if include_default
565 671 statuses = statuses.compact.uniq.sort
566 672 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
567 673 end
568 674 end
569 675
570 676 def assigned_to_was
571 677 if assigned_to_id_changed? && assigned_to_id_was.present?
572 678 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
573 679 end
574 680 end
575 681
576 682 # Returns the mail adresses of users that should be notified
577 683 def recipients
578 684 notified = []
579 685 # Author and assignee are always notified unless they have been
580 686 # locked or don't want to be notified
581 687 notified << author if author
582 688 if assigned_to
583 689 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
584 690 end
585 691 if assigned_to_was
586 692 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
587 693 end
588 694 notified = notified.select {|u| u.active? && u.notify_about?(self)}
589 695
590 696 notified += project.notified_users
591 697 notified.uniq!
592 698 # Remove users that can not view the issue
593 699 notified.reject! {|user| !visible?(user)}
594 700 notified.collect(&:mail)
595 701 end
596 702
597 703 # Returns the number of hours spent on this issue
598 704 def spent_hours
599 705 @spent_hours ||= time_entries.sum(:hours) || 0
600 706 end
601 707
602 708 # Returns the total number of hours spent on this issue and its descendants
603 709 #
604 710 # Example:
605 711 # spent_hours => 0.0
606 712 # spent_hours => 50.2
607 713 def total_spent_hours
608 714 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
609 715 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
610 716 end
611 717
612 718 def relations
613 719 @relations ||= (relations_from + relations_to).sort
614 720 end
615 721
616 722 # Preloads relations for a collection of issues
617 723 def self.load_relations(issues)
618 724 if issues.any?
619 725 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
620 726 issues.each do |issue|
621 727 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
622 728 end
623 729 end
624 730 end
625 731
626 732 # Preloads visible spent time for a collection of issues
627 733 def self.load_visible_spent_hours(issues, user=User.current)
628 734 if issues.any?
629 735 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
630 736 issues.each do |issue|
631 737 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
632 738 end
633 739 end
634 740 end
635 741
636 742 # Finds an issue relation given its id.
637 743 def find_relation(relation_id)
638 744 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
639 745 end
640 746
641 747 def all_dependent_issues(except=[])
642 748 except << self
643 749 dependencies = []
644 750 relations_from.each do |relation|
645 751 if relation.issue_to && !except.include?(relation.issue_to)
646 752 dependencies << relation.issue_to
647 753 dependencies += relation.issue_to.all_dependent_issues(except)
648 754 end
649 755 end
650 756 dependencies
651 757 end
652 758
653 759 # Returns an array of issues that duplicate this one
654 760 def duplicates
655 761 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
656 762 end
657 763
658 764 # Returns the due date or the target due date if any
659 765 # Used on gantt chart
660 766 def due_before
661 767 due_date || (fixed_version ? fixed_version.effective_date : nil)
662 768 end
663 769
664 770 # Returns the time scheduled for this issue.
665 771 #
666 772 # Example:
667 773 # Start Date: 2/26/09, End Date: 3/04/09
668 774 # duration => 6
669 775 def duration
670 776 (start_date && due_date) ? due_date - start_date : 0
671 777 end
672 778
673 779 def soonest_start
674 780 @soonest_start ||= (
675 781 relations_to.collect{|relation| relation.successor_soonest_start} +
676 782 ancestors.collect(&:soonest_start)
677 783 ).compact.max
678 784 end
679 785
680 786 def reschedule_after(date)
681 787 return if date.nil?
682 788 if leaf?
683 789 if start_date.nil? || start_date < date
684 790 self.start_date, self.due_date = date, date + duration
685 791 begin
686 792 save
687 793 rescue ActiveRecord::StaleObjectError
688 794 reload
689 795 self.start_date, self.due_date = date, date + duration
690 796 save
691 797 end
692 798 end
693 799 else
694 800 leaves.each do |leaf|
695 801 leaf.reschedule_after(date)
696 802 end
697 803 end
698 804 end
699 805
700 806 def <=>(issue)
701 807 if issue.nil?
702 808 -1
703 809 elsif root_id != issue.root_id
704 810 (root_id || 0) <=> (issue.root_id || 0)
705 811 else
706 812 (lft || 0) <=> (issue.lft || 0)
707 813 end
708 814 end
709 815
710 816 def to_s
711 817 "#{tracker} ##{id}: #{subject}"
712 818 end
713 819
714 820 # Returns a string of css classes that apply to the issue
715 821 def css_classes
716 822 s = "issue status-#{status.position} priority-#{priority.position}"
717 823 s << ' closed' if closed?
718 824 s << ' overdue' if overdue?
719 825 s << ' child' if child?
720 826 s << ' parent' unless leaf?
721 827 s << ' private' if is_private?
722 828 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
723 829 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
724 830 s
725 831 end
726 832
727 833 # Saves an issue and a time_entry from the parameters
728 834 def save_issue_with_child_records(params, existing_time_entry=nil)
729 835 Issue.transaction do
730 836 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
731 837 @time_entry = existing_time_entry || TimeEntry.new
732 838 @time_entry.project = project
733 839 @time_entry.issue = self
734 840 @time_entry.user = User.current
735 841 @time_entry.spent_on = User.current.today
736 842 @time_entry.attributes = params[:time_entry]
737 843 self.time_entries << @time_entry
738 844 end
739 845
740 846 # TODO: Rename hook
741 847 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
742 848 if save
743 849 # TODO: Rename hook
744 850 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
745 851 else
746 852 raise ActiveRecord::Rollback
747 853 end
748 854 end
749 855 end
750 856
751 857 # Unassigns issues from +version+ if it's no longer shared with issue's project
752 858 def self.update_versions_from_sharing_change(version)
753 859 # Update issues assigned to the version
754 860 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
755 861 end
756 862
757 863 # Unassigns issues from versions that are no longer shared
758 864 # after +project+ was moved
759 865 def self.update_versions_from_hierarchy_change(project)
760 866 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
761 867 # Update issues of the moved projects and issues assigned to a version of a moved project
762 868 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
763 869 end
764 870
765 871 def parent_issue_id=(arg)
766 872 parent_issue_id = arg.blank? ? nil : arg.to_i
767 873 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
768 874 @parent_issue.id
769 875 else
770 876 @parent_issue = nil
771 877 nil
772 878 end
773 879 end
774 880
775 881 def parent_issue_id
776 882 if instance_variable_defined? :@parent_issue
777 883 @parent_issue.nil? ? nil : @parent_issue.id
778 884 else
779 885 parent_id
780 886 end
781 887 end
782 888
783 889 # Extracted from the ReportsController.
784 890 def self.by_tracker(project)
785 891 count_and_group_by(:project => project,
786 892 :field => 'tracker_id',
787 893 :joins => Tracker.table_name)
788 894 end
789 895
790 896 def self.by_version(project)
791 897 count_and_group_by(:project => project,
792 898 :field => 'fixed_version_id',
793 899 :joins => Version.table_name)
794 900 end
795 901
796 902 def self.by_priority(project)
797 903 count_and_group_by(:project => project,
798 904 :field => 'priority_id',
799 905 :joins => IssuePriority.table_name)
800 906 end
801 907
802 908 def self.by_category(project)
803 909 count_and_group_by(:project => project,
804 910 :field => 'category_id',
805 911 :joins => IssueCategory.table_name)
806 912 end
807 913
808 914 def self.by_assigned_to(project)
809 915 count_and_group_by(:project => project,
810 916 :field => 'assigned_to_id',
811 917 :joins => User.table_name)
812 918 end
813 919
814 920 def self.by_author(project)
815 921 count_and_group_by(:project => project,
816 922 :field => 'author_id',
817 923 :joins => User.table_name)
818 924 end
819 925
820 926 def self.by_subproject(project)
821 927 ActiveRecord::Base.connection.select_all("select s.id as status_id,
822 928 s.is_closed as closed,
823 929 #{Issue.table_name}.project_id as project_id,
824 930 count(#{Issue.table_name}.id) as total
825 931 from
826 932 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
827 933 where
828 934 #{Issue.table_name}.status_id=s.id
829 935 and #{Issue.table_name}.project_id = #{Project.table_name}.id
830 936 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
831 937 and #{Issue.table_name}.project_id <> #{project.id}
832 938 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
833 939 end
834 940 # End ReportsController extraction
835 941
836 942 # Returns an array of projects that user can assign the issue to
837 943 def allowed_target_projects(user=User.current)
838 944 if new_record?
839 945 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
840 946 else
841 947 self.class.allowed_target_projects_on_move(user)
842 948 end
843 949 end
844 950
845 951 # Returns an array of projects that user can move issues to
846 952 def self.allowed_target_projects_on_move(user=User.current)
847 953 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
848 954 end
849 955
850 956 private
851 957
852 958 def after_project_change
853 959 # Update project_id on related time entries
854 960 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
855 961
856 962 # Delete issue relations
857 963 unless Setting.cross_project_issue_relations?
858 964 relations_from.clear
859 965 relations_to.clear
860 966 end
861 967
862 968 # Move subtasks
863 969 children.each do |child|
864 970 # Change project and keep project
865 971 child.send :project=, project, true
866 972 unless child.save
867 973 raise ActiveRecord::Rollback
868 974 end
869 975 end
870 976 end
871 977
872 978 def update_nested_set_attributes
873 979 if root_id.nil?
874 980 # issue was just created
875 981 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
876 982 set_default_left_and_right
877 983 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
878 984 if @parent_issue
879 985 move_to_child_of(@parent_issue)
880 986 end
881 987 reload
882 988 elsif parent_issue_id != parent_id
883 989 former_parent_id = parent_id
884 990 # moving an existing issue
885 991 if @parent_issue && @parent_issue.root_id == root_id
886 992 # inside the same tree
887 993 move_to_child_of(@parent_issue)
888 994 else
889 995 # to another tree
890 996 unless root?
891 997 move_to_right_of(root)
892 998 reload
893 999 end
894 1000 old_root_id = root_id
895 1001 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
896 1002 target_maxright = nested_set_scope.maximum(right_column_name) || 0
897 1003 offset = target_maxright + 1 - lft
898 1004 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
899 1005 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
900 1006 self[left_column_name] = lft + offset
901 1007 self[right_column_name] = rgt + offset
902 1008 if @parent_issue
903 1009 move_to_child_of(@parent_issue)
904 1010 end
905 1011 end
906 1012 reload
907 1013 # delete invalid relations of all descendants
908 1014 self_and_descendants.each do |issue|
909 1015 issue.relations.each do |relation|
910 1016 relation.destroy unless relation.valid?
911 1017 end
912 1018 end
913 1019 # update former parent
914 1020 recalculate_attributes_for(former_parent_id) if former_parent_id
915 1021 end
916 1022 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
917 1023 end
918 1024
919 1025 def update_parent_attributes
920 1026 recalculate_attributes_for(parent_id) if parent_id
921 1027 end
922 1028
923 1029 def recalculate_attributes_for(issue_id)
924 1030 if issue_id && p = Issue.find_by_id(issue_id)
925 1031 # priority = highest priority of children
926 1032 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
927 1033 p.priority = IssuePriority.find_by_position(priority_position)
928 1034 end
929 1035
930 1036 # start/due dates = lowest/highest dates of children
931 1037 p.start_date = p.children.minimum(:start_date)
932 1038 p.due_date = p.children.maximum(:due_date)
933 1039 if p.start_date && p.due_date && p.due_date < p.start_date
934 1040 p.start_date, p.due_date = p.due_date, p.start_date
935 1041 end
936 1042
937 1043 # done ratio = weighted average ratio of leaves
938 1044 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
939 1045 leaves_count = p.leaves.count
940 1046 if leaves_count > 0
941 1047 average = p.leaves.average(:estimated_hours).to_f
942 1048 if average == 0
943 1049 average = 1
944 1050 end
945 1051 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
946 1052 progress = done / (average * leaves_count)
947 1053 p.done_ratio = progress.round
948 1054 end
949 1055 end
950 1056
951 1057 # estimate = sum of leaves estimates
952 1058 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
953 1059 p.estimated_hours = nil if p.estimated_hours == 0.0
954 1060
955 1061 # ancestors will be recursively updated
956 1062 p.save(:validate => false)
957 1063 end
958 1064 end
959 1065
960 1066 # Update issues so their versions are not pointing to a
961 1067 # fixed_version that is not shared with the issue's project
962 1068 def self.update_versions(conditions=nil)
963 1069 # Only need to update issues with a fixed_version from
964 1070 # a different project and that is not systemwide shared
965 1071 Issue.scoped(:conditions => conditions).all(
966 1072 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
967 1073 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
968 1074 " AND #{Version.table_name}.sharing <> 'system'",
969 1075 :include => [:project, :fixed_version]
970 1076 ).each do |issue|
971 1077 next if issue.project.nil? || issue.fixed_version.nil?
972 1078 unless issue.project.shared_versions.include?(issue.fixed_version)
973 1079 issue.init_journal(User.current)
974 1080 issue.fixed_version = nil
975 1081 issue.save
976 1082 end
977 1083 end
978 1084 end
979 1085
980 1086 # Callback on attachment deletion
981 1087 def attachment_added(obj)
982 1088 if @current_journal && !obj.new_record?
983 1089 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
984 1090 end
985 1091 end
986 1092
987 1093 # Callback on attachment deletion
988 1094 def attachment_removed(obj)
989 1095 if @current_journal && !obj.new_record?
990 1096 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
991 1097 @current_journal.save
992 1098 end
993 1099 end
994 1100
995 1101 # Default assignment based on category
996 1102 def default_assign
997 1103 if assigned_to.nil? && category && category.assigned_to
998 1104 self.assigned_to = category.assigned_to
999 1105 end
1000 1106 end
1001 1107
1002 1108 # Updates start/due dates of following issues
1003 1109 def reschedule_following_issues
1004 1110 if start_date_changed? || due_date_changed?
1005 1111 relations_from.each do |relation|
1006 1112 relation.set_issue_to_dates
1007 1113 end
1008 1114 end
1009 1115 end
1010 1116
1011 1117 # Closes duplicates if the issue is being closed
1012 1118 def close_duplicates
1013 1119 if closing?
1014 1120 duplicates.each do |duplicate|
1015 1121 # Reload is need in case the duplicate was updated by a previous duplicate
1016 1122 duplicate.reload
1017 1123 # Don't re-close it if it's already closed
1018 1124 next if duplicate.closed?
1019 1125 # Same user and notes
1020 1126 if @current_journal
1021 1127 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1022 1128 end
1023 1129 duplicate.update_attribute :status, self.status
1024 1130 end
1025 1131 end
1026 1132 end
1027 1133
1028 1134 # Make sure updated_on is updated when adding a note
1029 1135 def force_updated_on_change
1030 1136 if @current_journal
1031 1137 self.updated_on = current_time_from_proper_timezone
1032 1138 end
1033 1139 end
1034 1140
1035 1141 # Saves the changes in a Journal
1036 1142 # Called after_save
1037 1143 def create_journal
1038 1144 if @current_journal
1039 1145 # attributes changes
1040 1146 if @attributes_before_change
1041 1147 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1042 1148 before = @attributes_before_change[c]
1043 1149 after = send(c)
1044 1150 next if before == after || (before.blank? && after.blank?)
1045 1151 @current_journal.details << JournalDetail.new(:property => 'attr',
1046 1152 :prop_key => c,
1047 1153 :old_value => before,
1048 1154 :value => after)
1049 1155 }
1050 1156 end
1051 1157 if @custom_values_before_change
1052 1158 # custom fields changes
1053 1159 custom_field_values.each {|c|
1054 1160 before = @custom_values_before_change[c.custom_field_id]
1055 1161 after = c.value
1056 1162 next if before == after || (before.blank? && after.blank?)
1057 1163
1058 1164 if before.is_a?(Array) || after.is_a?(Array)
1059 1165 before = [before] unless before.is_a?(Array)
1060 1166 after = [after] unless after.is_a?(Array)
1061 1167
1062 1168 # values removed
1063 1169 (before - after).reject(&:blank?).each do |value|
1064 1170 @current_journal.details << JournalDetail.new(:property => 'cf',
1065 1171 :prop_key => c.custom_field_id,
1066 1172 :old_value => value,
1067 1173 :value => nil)
1068 1174 end
1069 1175 # values added
1070 1176 (after - before).reject(&:blank?).each do |value|
1071 1177 @current_journal.details << JournalDetail.new(:property => 'cf',
1072 1178 :prop_key => c.custom_field_id,
1073 1179 :old_value => nil,
1074 1180 :value => value)
1075 1181 end
1076 1182 else
1077 1183 @current_journal.details << JournalDetail.new(:property => 'cf',
1078 1184 :prop_key => c.custom_field_id,
1079 1185 :old_value => before,
1080 1186 :value => after)
1081 1187 end
1082 1188 }
1083 1189 end
1084 1190 @current_journal.save
1085 1191 # reset current journal
1086 1192 init_journal @current_journal.user, @current_journal.notes
1087 1193 end
1088 1194 end
1089 1195
1090 1196 # Query generator for selecting groups of issue counts for a project
1091 1197 # based on specific criteria
1092 1198 #
1093 1199 # Options
1094 1200 # * project - Project to search in.
1095 1201 # * field - String. Issue field to key off of in the grouping.
1096 1202 # * joins - String. The table name to join against.
1097 1203 def self.count_and_group_by(options)
1098 1204 project = options.delete(:project)
1099 1205 select_field = options.delete(:field)
1100 1206 joins = options.delete(:joins)
1101 1207
1102 1208 where = "#{Issue.table_name}.#{select_field}=j.id"
1103 1209
1104 1210 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1105 1211 s.is_closed as closed,
1106 1212 j.id as #{select_field},
1107 1213 count(#{Issue.table_name}.id) as total
1108 1214 from
1109 1215 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1110 1216 where
1111 1217 #{Issue.table_name}.status_id=s.id
1112 1218 and #{where}
1113 1219 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1114 1220 and #{visible_condition(User.current, :project => project)}
1115 1221 group by s.id, s.is_closed, j.id")
1116 1222 end
1117 1223 end
@@ -1,104 +1,104
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 has_many :workflows, :foreign_key => "old_status_id"
20 has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id"
21 21 acts_as_list
22 22
23 before_destroy :delete_workflows
23 before_destroy :delete_workflow_rules
24 24 after_save :update_default
25 25
26 26 validates_presence_of :name
27 27 validates_uniqueness_of :name
28 28 validates_length_of :name, :maximum => 30
29 29 validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
30 30
31 31 scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
32 32
33 33 def update_default
34 34 IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
35 35 end
36 36
37 37 # Returns the default status for new issues
38 38 def self.default
39 39 find(:first, :conditions =>["is_default=?", true])
40 40 end
41 41
42 42 # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
43 43 def self.update_issue_done_ratios
44 44 if Issue.use_status_for_done_ratio?
45 45 IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
46 46 Issue.update_all(["done_ratio = ?", status.default_done_ratio],
47 47 ["status_id = ?", status.id])
48 48 end
49 49 end
50 50
51 51 return Issue.use_status_for_done_ratio?
52 52 end
53 53
54 54 # Returns an array of all statuses the given role can switch to
55 55 # Uses association cache when called more than one time
56 56 def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
57 57 if roles && tracker
58 58 role_ids = roles.collect(&:id)
59 59 transitions = workflows.select do |w|
60 60 role_ids.include?(w.role_id) &&
61 61 w.tracker_id == tracker.id &&
62 62 ((!w.author && !w.assignee) || (author && w.author) || (assignee && w.assignee))
63 63 end
64 64 transitions.collect{|w| w.new_status}.compact.sort
65 65 else
66 66 []
67 67 end
68 68 end
69 69
70 70 # Same thing as above but uses a database query
71 71 # More efficient than the previous method if called just once
72 72 def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
73 73 if roles.present? && tracker
74 74 conditions = "(author = :false AND assignee = :false)"
75 75 conditions << " OR author = :true" if author
76 76 conditions << " OR assignee = :true" if assignee
77 77
78 78 workflows.find(:all,
79 79 :include => :new_status,
80 80 :conditions => ["role_id IN (:role_ids) AND tracker_id = :tracker_id AND (#{conditions})",
81 81 {:role_ids => roles.collect(&:id), :tracker_id => tracker.id, :true => true, :false => false}
82 82 ]
83 83 ).collect{|w| w.new_status}.compact.sort
84 84 else
85 85 []
86 86 end
87 87 end
88 88
89 89 def <=>(status)
90 90 position <=> status.position
91 91 end
92 92
93 93 def to_s; name end
94 94
95 95 private
96 96 def check_integrity
97 97 raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
98 98 end
99 99
100 100 # Deletes associated workflows
101 def delete_workflows
102 Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
101 def delete_workflow_rules
102 WorkflowRule.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
103 103 end
104 104 end
@@ -1,194 +1,194
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 Role < ActiveRecord::Base
19 19 # Custom coder for the permissions attribute that should be an
20 20 # array of symbols. Rails 3 uses Psych which can be *unbelievably*
21 21 # slow on some platforms (eg. mingw32).
22 22 class PermissionsAttributeCoder
23 23 def self.load(str)
24 24 str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
25 25 end
26 26
27 27 def self.dump(value)
28 28 YAML.dump(value)
29 29 end
30 30 end
31 31
32 32 # Built-in roles
33 33 BUILTIN_NON_MEMBER = 1
34 34 BUILTIN_ANONYMOUS = 2
35 35
36 36 ISSUES_VISIBILITY_OPTIONS = [
37 37 ['all', :label_issues_visibility_all],
38 38 ['default', :label_issues_visibility_public],
39 39 ['own', :label_issues_visibility_own]
40 40 ]
41 41
42 42 scope :sorted, order("#{table_name}.builtin ASC, #{table_name}.position ASC")
43 43 scope :givable, order("#{table_name}.position ASC").where(:builtin => 0)
44 44 scope :builtin, lambda { |*args|
45 45 compare = (args.first == true ? 'not' : '')
46 46 where("#{compare} builtin = 0")
47 47 }
48 48
49 49 before_destroy :check_deletable
50 has_many :workflows, :dependent => :delete_all do
50 has_many :workflow_rules, :dependent => :delete_all do
51 51 def copy(source_role)
52 Workflow.copy(nil, source_role, nil, proxy_association.owner)
52 WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
53 53 end
54 54 end
55 55
56 56 has_many :member_roles, :dependent => :destroy
57 57 has_many :members, :through => :member_roles
58 58 acts_as_list
59 59
60 60 serialize :permissions, ::Role::PermissionsAttributeCoder
61 61 attr_protected :builtin
62 62
63 63 validates_presence_of :name
64 64 validates_uniqueness_of :name
65 65 validates_length_of :name, :maximum => 30
66 66 validates_inclusion_of :issues_visibility,
67 67 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
68 68 :if => lambda {|role| role.respond_to?(:issues_visibility)}
69 69
70 70 def permissions=(perms)
71 71 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
72 72 write_attribute(:permissions, perms)
73 73 end
74 74
75 75 def add_permission!(*perms)
76 76 self.permissions = [] unless permissions.is_a?(Array)
77 77
78 78 permissions_will_change!
79 79 perms.each do |p|
80 80 p = p.to_sym
81 81 permissions << p unless permissions.include?(p)
82 82 end
83 83 save!
84 84 end
85 85
86 86 def remove_permission!(*perms)
87 87 return unless permissions.is_a?(Array)
88 88 permissions_will_change!
89 89 perms.each { |p| permissions.delete(p.to_sym) }
90 90 save!
91 91 end
92 92
93 93 # Returns true if the role has the given permission
94 94 def has_permission?(perm)
95 95 !permissions.nil? && permissions.include?(perm.to_sym)
96 96 end
97 97
98 98 def <=>(role)
99 99 if role
100 100 if builtin == role.builtin
101 101 position <=> role.position
102 102 else
103 103 builtin <=> role.builtin
104 104 end
105 105 else
106 106 -1
107 107 end
108 108 end
109 109
110 110 def to_s
111 111 name
112 112 end
113 113
114 114 def name
115 115 case builtin
116 116 when 1; l(:label_role_non_member, :default => read_attribute(:name))
117 117 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
118 118 else; read_attribute(:name)
119 119 end
120 120 end
121 121
122 122 # Return true if the role is a builtin role
123 123 def builtin?
124 124 self.builtin != 0
125 125 end
126 126
127 127 # Return true if the role is a project member role
128 128 def member?
129 129 !self.builtin?
130 130 end
131 131
132 132 # Return true if role is allowed to do the specified action
133 133 # action can be:
134 134 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
135 135 # * a permission Symbol (eg. :edit_project)
136 136 def allowed_to?(action)
137 137 if action.is_a? Hash
138 138 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
139 139 else
140 140 allowed_permissions.include? action
141 141 end
142 142 end
143 143
144 144 # Return all the permissions that can be given to the role
145 145 def setable_permissions
146 146 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
147 147 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
148 148 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
149 149 setable_permissions
150 150 end
151 151
152 152 # Find all the roles that can be given to a project member
153 153 def self.find_all_givable
154 154 Role.givable.all
155 155 end
156 156
157 157 # Return the builtin 'non member' role. If the role doesn't exist,
158 158 # it will be created on the fly.
159 159 def self.non_member
160 160 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
161 161 end
162 162
163 163 # Return the builtin 'anonymous' role. If the role doesn't exist,
164 164 # it will be created on the fly.
165 165 def self.anonymous
166 166 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
167 167 end
168 168
169 169 private
170 170
171 171 def allowed_permissions
172 172 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
173 173 end
174 174
175 175 def allowed_actions
176 176 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
177 177 end
178 178
179 179 def check_deletable
180 180 raise "Can't delete role" if members.any?
181 181 raise "Can't delete builtin role" if builtin?
182 182 end
183 183
184 184 def self.find_or_create_system_role(builtin, name)
185 185 role = where(:builtin => builtin).first
186 186 if role.nil?
187 187 role = create(:name => name, :position => 0) do |r|
188 188 r.builtin = builtin
189 189 end
190 190 raise "Unable to create the #{name} role." if role.new_record?
191 191 end
192 192 role
193 193 end
194 194 end
@@ -1,104 +1,107
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 Tracker < ActiveRecord::Base
19 19
20 # Other fields should be appended, not inserted!
21 CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio)
20 CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze
21 # Fields that can be disabled
22 # Other (future) fields should be appended, not inserted!
23 CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze
24 CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze
22 25
23 26 before_destroy :check_integrity
24 27 has_many :issues
25 has_many :workflows, :dependent => :delete_all do
28 has_many :workflow_rules, :dependent => :delete_all do
26 29 def copy(source_tracker)
27 Workflow.copy(source_tracker, nil, proxy_association.owner, nil)
30 WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil)
28 31 end
29 32 end
30 33
31 34 has_and_belongs_to_many :projects
32 35 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
33 36 acts_as_list
34 37
35 38 attr_protected :field_bits
36 39
37 40 validates_presence_of :name
38 41 validates_uniqueness_of :name
39 42 validates_length_of :name, :maximum => 30
40 43
41 44 scope :sorted, order("#{table_name}.position ASC")
42 45 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
43 46
44 47 def to_s; name end
45 48
46 49 def <=>(tracker)
47 50 position <=> tracker.position
48 51 end
49 52
50 53 # Returns an array of IssueStatus that are used
51 54 # in the tracker's workflows
52 55 def issue_statuses
53 56 if @issue_statuses
54 57 return @issue_statuses
55 58 elsif new_record?
56 59 return []
57 60 end
58 61
59 ids = Workflow.
60 connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}").
62 ids = WorkflowTransition.
63 connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE tracker_id = #{id} AND type = 'WorkflowTransition'").
61 64 flatten.
62 65 uniq
63 66
64 67 @issue_statuses = IssueStatus.find_all_by_id(ids).sort
65 68 end
66 69
67 70 def disabled_core_fields
68 71 i = -1
69 72 @disabled_core_fields ||= CORE_FIELDS.select { i += 1; (fields_bits || 0) & (2 ** i) != 0}
70 73 end
71 74
72 75 def core_fields
73 76 CORE_FIELDS - disabled_core_fields
74 77 end
75 78
76 79 def core_fields=(fields)
77 80 raise ArgumentError.new("Tracker.core_fields takes an array") unless fields.is_a?(Array)
78 81
79 82 bits = 0
80 83 CORE_FIELDS.each_with_index do |field, i|
81 84 unless fields.include?(field)
82 85 bits |= 2 ** i
83 86 end
84 87 end
85 88 self.fields_bits = bits
86 89 @disabled_core_fields = nil
87 90 core_fields
88 91 end
89 92
90 93 # Returns the fields that are disabled for all the given trackers
91 94 def self.disabled_core_fields(trackers)
92 95 trackers.uniq.map(&:disabled_core_fields).reduce(:&)
93 96 end
94 97
95 98 # Returns the fields that are enabled for one tracker at least
96 99 def self.core_fields(trackers)
97 100 trackers.uniq.map(&:core_fields).reduce(:|)
98 101 end
99 102
100 103 private
101 104 def check_integrity
102 105 raise Exception.new("Can't delete tracker") if Issue.where(:tracker_id => self.id).any?
103 106 end
104 107 end
@@ -1,89 +1,73
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 class Workflow < ActiveRecord::Base
18 class WorkflowRule < ActiveRecord::Base
19 self.table_name = "#{table_name_prefix}workflows#{table_name_suffix}"
20
19 21 belongs_to :role
22 belongs_to :tracker
20 23 belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id'
21 24 belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id'
22 25
23 validates_presence_of :role, :old_status, :new_status
24
25 # Returns workflow transitions count by tracker and role
26 def self.count_by_tracker_and_role
27 counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{Workflow.table_name} GROUP BY role_id, tracker_id")
28 roles = Role.sorted.all
29 trackers = Tracker.sorted.all
30
31 result = []
32 trackers.each do |tracker|
33 t = []
34 roles.each do |role|
35 row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s}
36 t << [role, (row.nil? ? 0 : row['c'].to_i)]
37 end
38 result << [tracker, t]
39 end
40
41 result
42 end
26 validates_presence_of :role, :tracker, :old_status
43 27
44 28 # Copies workflows from source to targets
45 29 def self.copy(source_tracker, source_role, target_trackers, target_roles)
46 30 unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role)
47 31 raise ArgumentError.new("source_tracker or source_role must be specified")
48 32 end
49 33
50 34 target_trackers = [target_trackers].flatten.compact
51 35 target_roles = [target_roles].flatten.compact
52 36
53 37 target_trackers = Tracker.sorted.all if target_trackers.empty?
54 38 target_roles = Role.all if target_roles.empty?
55 39
56 40 target_trackers.each do |target_tracker|
57 41 target_roles.each do |target_role|
58 42 copy_one(source_tracker || target_tracker,
59 43 source_role || target_role,
60 44 target_tracker,
61 45 target_role)
62 46 end
63 47 end
64 48 end
65 49
66 50 # Copies a single set of workflows from source to target
67 51 def self.copy_one(source_tracker, source_role, target_tracker, target_role)
68 52 unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? &&
69 53 source_role.is_a?(Role) && !source_role.new_record? &&
70 54 target_tracker.is_a?(Tracker) && !target_tracker.new_record? &&
71 55 target_role.is_a?(Role) && !target_role.new_record?
72 56
73 57 raise ArgumentError.new("arguments can not be nil or unsaved objects")
74 58 end
75 59
76 60 if source_tracker == target_tracker && source_role == target_role
77 61 false
78 62 else
79 63 transaction do
80 64 delete_all :tracker_id => target_tracker.id, :role_id => target_role.id
81 connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee)" +
82 " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee" +
83 " FROM #{Workflow.table_name}" +
65 connection.insert "INSERT INTO #{WorkflowRule.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee, field_name, rule, type)" +
66 " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee, field_name, rule, type" +
67 " FROM #{WorkflowRule.table_name}" +
84 68 " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}"
85 69 end
86 70 true
87 71 end
88 72 end
89 73 end
@@ -1,68 +1,71
1 1 <%= labelled_fields_for :issue, @issue do |f| %>
2 2
3 3 <div class="splitcontent">
4 4 <div class="splitcontentleft">
5 5 <% if @issue.safe_attribute? 'status_id' %>
6 6 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
7 <%= observe_field :issue_status_id, :url => project_issue_form_path(@project, :id => @issue),
8 :with => "Form.serialize('issue-form')" %>
9
7 10 <% else %>
8 11 <p><label><%= l(:field_status) %></label> <%= h(@issue.status.name) %></p>
9 12 <% end %>
10 13
11 14 <% if @issue.safe_attribute? 'priority_id' %>
12 15 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true}, :disabled => !@issue.leaf? %></p>
13 16 <% end %>
14 17
15 18 <% if @issue.safe_attribute? 'assigned_to_id' %>
16 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true %></p>
19 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true, :required => @issue.required_attribute?('assigned_to_id') %></p>
17 20 <% end %>
18 21
19 22 <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %>
20 <p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
23 <p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true, :required => @issue.required_attribute?('category_id') %>
21 24 <%= link_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
22 25 {:url => new_project_issue_category_path(@issue.project), :method => 'get'},
23 26 :title => l(:label_issue_category_new),
24 27 :tabindex => 200) if User.current.allowed_to?(:manage_categories, @issue.project) %></p>
25 28 <% end %>
26 29
27 30 <% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %>
28 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %>
31 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true, :required => @issue.required_attribute?('fixed_version_id') %>
29 32 <%= link_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
30 33 {:url => new_project_version_path(@issue.project), :method => 'get'},
31 34 :title => l(:label_version_new),
32 35 :tabindex => 200) if User.current.allowed_to?(:manage_versions, @issue.project) %>
33 36 </p>
34 37 <% end %>
35 38 </div>
36 39
37 40 <div class="splitcontentright">
38 41 <% if @issue.safe_attribute? 'parent_issue_id' %>
39 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10 %></p>
42 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %></p>
40 43 <div id="parent_issue_candidates" class="autocomplete"></div>
41 44 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @issue.project) }')" %>
42 45 <% end %>
43 46
44 47 <% if @issue.safe_attribute? 'start_date' %>
45 <p><%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_start_date') if @issue.leaf? %></p>
48 <p><%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('start_date') %><%= calendar_for('issue_start_date') if @issue.leaf? %></p>
46 49 <% end %>
47 50
48 51 <% if @issue.safe_attribute? 'due_date' %>
49 <p><%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_due_date') if @issue.leaf? %></p>
52 <p><%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('due_date') %><%= calendar_for('issue_due_date') if @issue.leaf? %></p>
50 53 <% end %>
51 54
52 55 <% if @issue.safe_attribute? 'estimated_hours' %>
53 <p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf? %> <%= l(:field_hours) %></p>
56 <p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
54 57 <% end %>
55 58
56 59 <% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %>
57 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
60 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
58 61 <% end %>
59 62 </div>
60 63 </div>
61 64
62 65 <% if @issue.safe_attribute? 'custom_field_values' %>
63 66 <%= render :partial => 'issues/form_custom_fields' %>
64 67 <% end %>
65 68
66 69 <% end %>
67 70
68 71 <% include_calendar_headers_tags %>
@@ -1,48 +1,48
1 1 <%= labelled_fields_for :issue, @issue do |f| %>
2 2 <%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %>
3 3
4 4 <% if @issue.safe_attribute? 'is_private' %>
5 5 <p style="float:right; margin-right:1em;">
6 6 <label class="inline" for="issue_is_private" id="issue_is_private_label"><%= f.check_box :is_private, :no_label => true %> <%= l(:field_is_private) %></label>
7 7 </p>
8 8 <% end %>
9 9
10 10 <% if @issue.safe_attribute? 'project_id' %>
11 11 <p><%= f.select :project_id, project_tree_options_for_select(@issue.allowed_target_projects, :selected => @issue.project), :required => true %></p>
12 <%= observe_field :issue_project_id, :url => project_issue_form_path(@project, :id => @issue, :project_change => '1'),
12 <%= observe_field :issue_project_id, :url => project_issue_form_path(@project, :id => @issue),
13 13 :with => "Form.serialize('issue-form')" %>
14 14 <% end %>
15 15
16 16 <% if @issue.safe_attribute? 'tracker_id' %>
17 17 <p><%= f.select :tracker_id, @issue.project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
18 18 <%= observe_field :issue_tracker_id, :url => project_issue_form_path(@project, :id => @issue),
19 19 :with => "Form.serialize('issue-form')" %>
20 20 <% end %>
21 21
22 22 <% if @issue.safe_attribute? 'subject' %>
23 23 <p><%= f.text_field :subject, :size => 80, :required => true %></p>
24 24 <% end %>
25 25
26 26 <% if @issue.safe_attribute? 'description' %>
27 27 <p>
28 <label><%= l(:field_description) %></label>
28 <%= f.label_for_field :description, :required => @issue.required_attribute?('description') %>
29 29 <%= link_to_function image_tag('edit.png'),
30 30 'Element.hide(this); Effect.toggle("issue_description_and_toolbar", "appear", {duration:0.3})' unless @issue.new_record? %>
31 31 <%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %>
32 32 <%= f.text_area :description,
33 33 :cols => 60,
34 34 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
35 35 :accesskey => accesskey(:edit),
36 36 :class => 'wiki-edit',
37 37 :no_label => true %>
38 38 <% end %>
39 39 </p>
40 40 <%= wikitoolbar_for 'issue_description' %>
41 41 <% end %>
42 42
43 43 <div id="attributes" class="attributes">
44 44 <%= render :partial => 'issues/attributes' %>
45 45 </div>
46 46
47 47 <%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
48 48 <% end %>
@@ -1,13 +1,13
1 1 <div class="splitcontent">
2 2 <div class="splitcontentleft">
3 3 <% i = 0 %>
4 4 <% split_on = (@issue.custom_field_values.size / 2.0).ceil - 1 %>
5 <% @issue.custom_field_values.each do |value| %>
6 <p><%= custom_field_tag_with_label :issue, value %></p>
5 <% @issue.editable_custom_field_values.each do |value| %>
6 <p><%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %></p>
7 7 <% if i == split_on -%>
8 8 </div><div class="splitcontentright">
9 9 <% end -%>
10 10 <% i += 1 -%>
11 11 <% end -%>
12 12 </div>
13 13 </div>
@@ -1,30 +1,30
1 1 <div class="contextual">
2 2 <%= link_to l(:label_tracker_new), new_tracker_path, :class => 'icon icon-add' %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_tracker_plural)%></h2>
6 6
7 7 <table class="list">
8 8 <thead><tr>
9 9 <th><%=l(:label_tracker)%></th>
10 10 <th></th>
11 11 <th><%=l(:button_sort)%></th>
12 12 <th></th>
13 13 </tr></thead>
14 14 <tbody>
15 15 <% for tracker in @trackers %>
16 16 <tr class="<%= cycle("odd", "even") %>">
17 17 <td><%= link_to h(tracker.name), edit_tracker_path(tracker) %></td>
18 <td align="center"><% unless tracker.workflows.count > 0 %><span class="icon icon-warning"><%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), {:controller => 'workflows', :action => 'edit', :tracker_id => tracker} %>)</span><% end %></td>
18 <td align="center"><% unless tracker.workflow_rules.count > 0 %><span class="icon icon-warning"><%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), {:controller => 'workflows', :action => 'edit', :tracker_id => tracker} %>)</span><% end %></td>
19 19 <td align="center" style="width:15%;"><%= reorder_links('tracker', {:action => 'update', :id => tracker}, :put) %></td>
20 20 <td class="buttons">
21 21 <%= delete_link tracker_path(tracker) %>
22 22 </td>
23 23 </tr>
24 24 <% end %>
25 25 </tbody>
26 26 </table>
27 27
28 28 <p class="pagination"><%= pagination_links_full @tracker_pages %></p>
29 29
30 30 <% html_title(l(:label_tracker_plural)) -%>
@@ -1,50 +1,57
1 1 <%= render :partial => 'action_menu' %>
2 2
3 3 <h2><%=l(:label_workflow)%></h2>
4 4
5 <div class="tabs">
6 <ul>
7 <li><%= link_to 'Status transitions', {:action => 'edit', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %></li>
8 <li><%= link_to 'Fields permissions', {:action => 'permissions', :role_id => @role, :tracker_id => @tracker} %></li>
9 </ul>
10 </div>
11
5 12 <p><%=l(:text_workflow_edit)%>:</p>
6 13
7 14 <%= form_tag({}, :method => 'get') do %>
8 15 <p>
9 16 <label><%=l(:label_role)%>:
10 17 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %></label>
11 18
12 19 <label><%=l(:label_tracker)%>:
13 20 <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %></label>
14 21
22 <%= submit_tag l(:button_edit), :name => nil %>
23
15 24 <%= hidden_field_tag 'used_statuses_only', '0' %>
16 25 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
17 </p>
18 <p>
19 <%= submit_tag l(:button_edit), :name => nil %>
26
20 27 </p>
21 28 <% end %>
22 29
23 30 <% if @tracker && @role && @statuses.any? %>
24 31 <%= form_tag({}, :id => 'workflow_form' ) do %>
25 32 <%= hidden_field_tag 'tracker_id', @tracker.id %>
26 33 <%= hidden_field_tag 'role_id', @role.id %>
27 34 <div class="autoscroll">
28 35 <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
29 36
30 37 <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
31 38 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_author) %></legend>
32 39 <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
33 40 <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
34 41 </div>
35 42 </fieldset>
36 43 <%= javascript_tag "hideFieldset($('author_workflows'))" unless @workflows['author'].present? %>
37 44
38 45 <fieldset class="collapsible" style="padding: 0;">
39 46 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_assignee) %></legend>
40 47 <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
41 48 <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
42 49 </div>
43 50 </fieldset>
44 51 <%= javascript_tag "hideFieldset($('assignee_workflows'))" unless @workflows['assignee'].present? %>
45 52 </div>
46 53 <%= submit_tag l(:button_save) %>
47 54 <% end %>
48 55 <% end %>
49 56
50 57 <% html_title(l(:label_workflow)) -%>
@@ -1,339 +1,340
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 RedmineApp::Application.routes.draw do
19 19 root :to => 'welcome#index', :as => 'home'
20 20
21 21 match 'login', :to => 'account#login', :as => 'signin'
22 22 match 'logout', :to => 'account#logout', :as => 'signout'
23 23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 25 match 'account/activate', :to => 'account#activate', :via => :get
26 26
27 27 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news'
28 28 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue'
29 29 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue'
30 30 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue'
31 31
32 32 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
33 33 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
34 34
35 35 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post]
36 36 get 'boards/:board_id/topics/:id', :to => 'messages#show'
37 37 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
38 38 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
39 39
40 40 post 'boards/:board_id/topics/preview', :to => 'messages#preview'
41 41 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
42 42 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
43 43 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
44 44
45 45 # Misc issue routes. TODO: move into resources
46 46 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
47 47 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu'
48 48 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes'
49 49 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
50 50
51 51 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
52 52 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
53 53
54 54 match '/projects/:project_id/issues/gantt', :to => 'gantts#show'
55 55 match '/issues/gantt', :to => 'gantts#show'
56 56
57 57 match '/projects/:project_id/issues/calendar', :to => 'calendars#show'
58 58 match '/issues/calendar', :to => 'calendars#show'
59 59
60 60 match 'projects/:id/issues/report', :to => 'reports#issue_report', :via => :get
61 61 match 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :via => :get
62 62
63 63 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
64 64 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
65 65 match 'my/page', :controller => 'my', :action => 'page', :via => :get
66 66 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
67 67 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
68 68 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
69 69 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
70 70 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
71 71 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
72 72 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
73 73 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
74 74
75 75 resources :users
76 76 match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership'
77 77 match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete
78 78 match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
79 79
80 80 match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get
81 81 match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post
82 82 match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post
83 83 match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post
84 84 match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post
85 85 match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post
86 86 match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get
87 87
88 88 match 'projects/:id/settings/:tab', :to => "projects#settings"
89 89
90 90 resources :projects do
91 91 member do
92 92 get 'settings'
93 93 post 'modules'
94 94 post 'archive'
95 95 post 'unarchive'
96 96 post 'close'
97 97 post 'reopen'
98 98 match 'copy', :via => [:get, :post]
99 99 end
100 100
101 101 resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :create, :update, :destroy] do
102 102 collection do
103 103 get 'autocomplete'
104 104 end
105 105 end
106 106
107 107 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
108 108
109 109 match 'issues/:copy_from/copy', :to => 'issues#new'
110 110 resources :issues, :only => [:index, :new, :create] do
111 111 resources :time_entries, :controller => 'timelog' do
112 112 collection do
113 113 get 'report'
114 114 end
115 115 end
116 116 end
117 117 # issue form update
118 118 match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form'
119 119
120 120 resources :files, :only => [:index, :new, :create]
121 121
122 122 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
123 123 collection do
124 124 put 'close_completed'
125 125 end
126 126 end
127 127 match 'versions.:format', :to => 'versions#index'
128 128 match 'roadmap', :to => 'versions#index', :format => false
129 129 match 'versions', :to => 'versions#index'
130 130
131 131 resources :news, :except => [:show, :edit, :update, :destroy]
132 132 resources :time_entries, :controller => 'timelog' do
133 133 get 'report', :on => :collection
134 134 end
135 135 resources :queries, :only => [:new, :create]
136 136 resources :issue_categories, :shallow => true
137 137 resources :documents, :except => [:show, :edit, :update, :destroy]
138 138 resources :boards
139 139 resources :repositories, :shallow => true, :except => [:index, :show] do
140 140 member do
141 141 match 'committers', :via => [:get, :post]
142 142 end
143 143 end
144 144
145 145 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
146 146 match 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
147 147 match 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff'
148 148 resources :wiki, :except => [:index, :new, :create] do
149 149 member do
150 150 get 'rename'
151 151 post 'rename'
152 152 get 'history'
153 153 get 'diff'
154 154 match 'preview', :via => [:post, :put]
155 155 post 'protect'
156 156 post 'add_attachment'
157 157 end
158 158 collection do
159 159 get 'export'
160 160 get 'date_index'
161 161 end
162 162 end
163 163 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
164 164 match 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
165 165 end
166 166
167 167 resources :issues do
168 168 collection do
169 169 match 'bulk_edit', :via => [:get, :post]
170 170 post 'bulk_update'
171 171 end
172 172 resources :time_entries, :controller => 'timelog' do
173 173 collection do
174 174 get 'report'
175 175 end
176 176 end
177 177 resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
178 178 end
179 179 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
180 180
181 181 resources :queries, :except => [:show]
182 182
183 183 resources :news, :only => [:index, :show, :edit, :update, :destroy]
184 184 match '/news/:id/comments', :to => 'comments#create', :via => :post
185 185 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
186 186
187 187 resources :versions, :only => [:show, :edit, :update, :destroy] do
188 188 post 'status_by', :on => :member
189 189 end
190 190
191 191 resources :documents, :only => [:show, :edit, :update, :destroy] do
192 192 post 'add_attachment', :on => :member
193 193 end
194 194
195 195 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu
196 196
197 197 resources :time_entries, :controller => 'timelog', :except => :destroy do
198 198 collection do
199 199 get 'report'
200 200 get 'bulk_edit'
201 201 post 'bulk_update'
202 202 end
203 203 end
204 204 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
205 205 # TODO: delete /time_entries for bulk deletion
206 206 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
207 207
208 208 # TODO: port to be part of the resources route(s)
209 209 match 'projects/:id/settings/:tab', :to => 'projects#settings', :via => :get
210 210
211 211 get 'projects/:id/activity', :to => 'activities#index'
212 212 get 'projects/:id/activity.:format', :to => 'activities#index'
213 213 get 'activity', :to => 'activities#index'
214 214
215 215 # repositories routes
216 216 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
217 217 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
218 218
219 219 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
220 220 :to => 'repositories#changes'
221 221
222 222 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
223 223 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
224 224 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
225 225 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
226 226 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
227 227 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
228 228 :controller => 'repositories',
229 229 :format => false,
230 230 :constraints => {
231 231 :action => /(browse|show|entry|raw|annotate|diff)/,
232 232 :rev => /[a-z0-9\.\-_]+/
233 233 }
234 234
235 235 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
236 236 get 'projects/:id/repository/graph', :to => 'repositories#graph'
237 237
238 238 get 'projects/:id/repository/changes(/*path(.:ext))',
239 239 :to => 'repositories#changes'
240 240
241 241 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
242 242 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
243 243 get 'projects/:id/repository/revision', :to => 'repositories#revision'
244 244 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
245 245 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
246 246 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
247 247 :controller => 'repositories',
248 248 :format => false,
249 249 :constraints => {
250 250 :action => /(browse|show|entry|raw|annotate|diff)/,
251 251 :rev => /[a-z0-9\.\-_]+/
252 252 }
253 253 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
254 254 :controller => 'repositories',
255 255 :action => /(browse|show|entry|raw|changes|annotate|diff)/
256 256 get 'projects/:id/repository/:action(/*path(.:ext))',
257 257 :controller => 'repositories',
258 258 :action => /(browse|show|entry|raw|changes|annotate|diff)/
259 259
260 260 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
261 261 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
262 262
263 263 # additional routes for having the file name at the end of url
264 264 match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get
265 265 match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get
266 266 match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get
267 267 match 'attachments/thumbnail/:id', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get
268 268 resources :attachments, :only => [:show, :destroy]
269 269
270 270 resources :groups do
271 271 member do
272 272 get 'autocomplete_for_user'
273 273 end
274 274 end
275 275
276 276 match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users'
277 277 match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user'
278 278 match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post
279 279 match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post
280 280
281 281 resources :trackers, :except => :show
282 282 resources :issue_statuses, :except => :show do
283 283 collection do
284 284 post 'update_issue_done_ratio'
285 285 end
286 286 end
287 287 resources :custom_fields, :except => :show
288 288 resources :roles, :except => :show do
289 289 collection do
290 290 match 'permissions', :via => [:get, :post]
291 291 end
292 292 end
293 293 resources :enumerations, :except => :show
294 294
295 295 get 'projects/:id/search', :controller => 'search', :action => 'index'
296 296 get 'search', :controller => 'search', :action => 'index'
297 297
298 298 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
299 299
300 300 match 'admin', :controller => 'admin', :action => 'index', :via => :get
301 301 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
302 302 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
303 303 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
304 304 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
305 305 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
306 306
307 307 resources :auth_sources do
308 308 member do
309 309 get 'test_connection'
310 310 end
311 311 end
312 312
313 313 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
314 314 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
315 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
315 316 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
316 317 match 'settings', :controller => 'settings', :action => 'index', :via => :get
317 318 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
318 319 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post]
319 320
320 321 match 'sys/projects', :to => 'sys#projects', :via => :get
321 322 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
322 323 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get
323 324
324 325 match 'uploads', :to => 'attachments#upload', :via => :post
325 326
326 327 get 'robots.txt', :to => 'welcome#robots'
327 328
328 329 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
329 330 file = File.join(plugin_dir, "config/routes.rb")
330 331 if File.exists?(file)
331 332 begin
332 333 instance_eval File.read(file)
333 334 rescue Exception => e
334 335 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
335 336 exit 1
336 337 end
337 338 end
338 339 end
339 340 end
@@ -1,183 +1,183
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module Redmine
19 19 module DefaultData
20 20 class DataAlreadyLoaded < Exception; end
21 21
22 22 module Loader
23 23 include Redmine::I18n
24 24
25 25 class << self
26 26 # Returns true if no data is already loaded in the database
27 27 # otherwise false
28 28 def no_data?
29 29 !Role.find(:first, :conditions => {:builtin => 0}) &&
30 30 !Tracker.find(:first) &&
31 31 !IssueStatus.find(:first) &&
32 32 !Enumeration.find(:first)
33 33 end
34 34
35 35 # Loads the default data
36 36 # Raises a RecordNotSaved exception if something goes wrong
37 37 def load(lang=nil)
38 38 raise DataAlreadyLoaded.new("Some configuration data is already loaded.") unless no_data?
39 39 set_language_if_valid(lang)
40 40
41 41 Role.transaction do
42 42 # Roles
43 43 manager = Role.create! :name => l(:default_role_manager),
44 44 :issues_visibility => 'all',
45 45 :position => 1
46 46 manager.permissions = manager.setable_permissions.collect {|p| p.name}
47 47 manager.save!
48 48
49 49 developer = Role.create! :name => l(:default_role_developer),
50 50 :position => 2,
51 51 :permissions => [:manage_versions,
52 52 :manage_categories,
53 53 :view_issues,
54 54 :add_issues,
55 55 :edit_issues,
56 56 :manage_issue_relations,
57 57 :manage_subtasks,
58 58 :add_issue_notes,
59 59 :save_queries,
60 60 :view_gantt,
61 61 :view_calendar,
62 62 :log_time,
63 63 :view_time_entries,
64 64 :comment_news,
65 65 :view_documents,
66 66 :view_wiki_pages,
67 67 :view_wiki_edits,
68 68 :edit_wiki_pages,
69 69 :delete_wiki_pages,
70 70 :add_messages,
71 71 :edit_own_messages,
72 72 :view_files,
73 73 :manage_files,
74 74 :browse_repository,
75 75 :view_changesets,
76 76 :commit_access,
77 77 :manage_related_issues]
78 78
79 79 reporter = Role.create! :name => l(:default_role_reporter),
80 80 :position => 3,
81 81 :permissions => [:view_issues,
82 82 :add_issues,
83 83 :add_issue_notes,
84 84 :save_queries,
85 85 :view_gantt,
86 86 :view_calendar,
87 87 :log_time,
88 88 :view_time_entries,
89 89 :comment_news,
90 90 :view_documents,
91 91 :view_wiki_pages,
92 92 :view_wiki_edits,
93 93 :add_messages,
94 94 :edit_own_messages,
95 95 :view_files,
96 96 :browse_repository,
97 97 :view_changesets]
98 98
99 99 Role.non_member.update_attribute :permissions, [:view_issues,
100 100 :add_issues,
101 101 :add_issue_notes,
102 102 :save_queries,
103 103 :view_gantt,
104 104 :view_calendar,
105 105 :view_time_entries,
106 106 :comment_news,
107 107 :view_documents,
108 108 :view_wiki_pages,
109 109 :view_wiki_edits,
110 110 :add_messages,
111 111 :view_files,
112 112 :browse_repository,
113 113 :view_changesets]
114 114
115 115 Role.anonymous.update_attribute :permissions, [:view_issues,
116 116 :view_gantt,
117 117 :view_calendar,
118 118 :view_time_entries,
119 119 :view_documents,
120 120 :view_wiki_pages,
121 121 :view_wiki_edits,
122 122 :view_files,
123 123 :browse_repository,
124 124 :view_changesets]
125 125
126 126 # Trackers
127 127 Tracker.create!(:name => l(:default_tracker_bug), :is_in_chlog => true, :is_in_roadmap => false, :position => 1)
128 128 Tracker.create!(:name => l(:default_tracker_feature), :is_in_chlog => true, :is_in_roadmap => true, :position => 2)
129 129 Tracker.create!(:name => l(:default_tracker_support), :is_in_chlog => false, :is_in_roadmap => false, :position => 3)
130 130
131 131 # Issue statuses
132 132 new = IssueStatus.create!(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :position => 1)
133 133 in_progress = IssueStatus.create!(:name => l(:default_issue_status_in_progress), :is_closed => false, :is_default => false, :position => 2)
134 134 resolved = IssueStatus.create!(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :position => 3)
135 135 feedback = IssueStatus.create!(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :position => 4)
136 136 closed = IssueStatus.create!(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :position => 5)
137 137 rejected = IssueStatus.create!(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :position => 6)
138 138
139 139 # Workflow
140 140 Tracker.find(:all).each { |t|
141 141 IssueStatus.find(:all).each { |os|
142 142 IssueStatus.find(:all).each { |ns|
143 Workflow.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
143 WorkflowTransition.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
144 144 }
145 145 }
146 146 }
147 147
148 148 Tracker.find(:all).each { |t|
149 149 [new, in_progress, resolved, feedback].each { |os|
150 150 [in_progress, resolved, feedback, closed].each { |ns|
151 Workflow.create!(:tracker_id => t.id, :role_id => developer.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
151 WorkflowTransition.create!(:tracker_id => t.id, :role_id => developer.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
152 152 }
153 153 }
154 154 }
155 155
156 156 Tracker.find(:all).each { |t|
157 157 [new, in_progress, resolved, feedback].each { |os|
158 158 [closed].each { |ns|
159 Workflow.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
159 WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
160 160 }
161 161 }
162 Workflow.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id)
162 WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id)
163 163 }
164 164
165 165 # Enumerations
166 166 DocumentCategory.create!(:name => l(:default_doc_category_user), :position => 1)
167 167 DocumentCategory.create!(:name => l(:default_doc_category_tech), :position => 2)
168 168
169 169 IssuePriority.create!(:name => l(:default_priority_low), :position => 1)
170 170 IssuePriority.create!(:name => l(:default_priority_normal), :position => 2, :is_default => true)
171 171 IssuePriority.create!(:name => l(:default_priority_high), :position => 3)
172 172 IssuePriority.create!(:name => l(:default_priority_urgent), :position => 4)
173 173 IssuePriority.create!(:name => l(:default_priority_immediate), :position => 5)
174 174
175 175 TimeEntryActivity.create!(:name => l(:default_activity_design), :position => 1)
176 176 TimeEntryActivity.create!(:name => l(:default_activity_development), :position => 2)
177 177 end
178 178 true
179 179 end
180 180 end
181 181 end
182 182 end
183 183 end
@@ -1,1091 +1,1094
1 1 html {overflow-y:scroll;}
2 2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3 3
4 4 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
5 5 h1 {margin:0; padding:0; font-size: 24px;}
6 6 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
8 8 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
9 9
10 10 /***** Layout *****/
11 11 #wrapper {background: white;}
12 12
13 13 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
14 14 #top-menu ul {margin: 0; padding: 0;}
15 15 #top-menu li {
16 16 float:left;
17 17 list-style-type:none;
18 18 margin: 0px 0px 0px 0px;
19 19 padding: 0px 0px 0px 0px;
20 20 white-space:nowrap;
21 21 }
22 22 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
23 23 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
24 24
25 25 #account {float:right;}
26 26
27 27 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
28 28 #header a {color:#f8f8f8;}
29 29 #header h1 a.ancestor { font-size: 80%; }
30 30 #quick-search {float:right;}
31 31
32 32 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
33 33 #main-menu ul {margin: 0; padding: 0;}
34 34 #main-menu li {
35 35 float:left;
36 36 list-style-type:none;
37 37 margin: 0px 2px 0px 0px;
38 38 padding: 0px 0px 0px 0px;
39 39 white-space:nowrap;
40 40 }
41 41 #main-menu li a {
42 42 display: block;
43 43 color: #fff;
44 44 text-decoration: none;
45 45 font-weight: bold;
46 46 margin: 0;
47 47 padding: 4px 10px 4px 10px;
48 48 }
49 49 #main-menu li a:hover {background:#759FCF; color:#fff;}
50 50 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
51 51
52 52 #admin-menu ul {margin: 0; padding: 0;}
53 53 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
54 54
55 55 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
56 56 #admin-menu a.projects { background-image: url(../images/projects.png); }
57 57 #admin-menu a.users { background-image: url(../images/user.png); }
58 58 #admin-menu a.groups { background-image: url(../images/group.png); }
59 59 #admin-menu a.roles { background-image: url(../images/database_key.png); }
60 60 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
61 61 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
62 62 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
63 63 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
64 64 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
65 65 #admin-menu a.settings { background-image: url(../images/changeset.png); }
66 66 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
67 67 #admin-menu a.info { background-image: url(../images/help.png); }
68 68 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
69 69
70 70 #main {background-color:#EEEEEE;}
71 71
72 72 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
73 73 * html #sidebar{ width: 22%; }
74 74 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
75 75 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
76 76 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
77 77 #sidebar .contextual { margin-right: 1em; }
78 78
79 79 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
80 80 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
81 81 html>body #content { min-height: 600px; }
82 82 * html body #content { height: 600px; } /* IE */
83 83
84 84 #main.nosidebar #sidebar{ display: none; }
85 85 #main.nosidebar #content{ width: auto; border-right: 0; }
86 86
87 87 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
88 88
89 89 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
90 90 #login-form table td {padding: 6px;}
91 91 #login-form label {font-weight: bold;}
92 92 #login-form input#username, #login-form input#password { width: 300px; }
93 93
94 94 #modalbg {position:absolute; top:0; left:0; width:100%; height:100%; background:#ccc; z-index:49; opacity:0.5;}
95 95 html>body #modalbg {position:fixed;}
96 96 div.modal { border-radius:5px; position:absolute; top:25%; background:#fff; border:2px solid #759FCF; z-index:50; padding:0px; padding:8px; box-shadow: 1px 1px 8px #888; }
97 97 div.modal h3.title {background:#759FCF; color:#fff; border:0; padding-left:8px; margin:-8px; margin-bottom: 1em; border-top-left-radius:2px;border-top-right-radius:2px;}
98 98 div.modal p.buttons {text-align:right; margin-bottom:0;}
99 99 html>body div.modal {position:fixed;}
100 100
101 101 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
102 102
103 103 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
104 104
105 105 /***** Links *****/
106 106 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
107 107 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
108 108 a img{ border: 0; }
109 109
110 110 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
111 111 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
112 112
113 113 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px; -moz-border-radius:2px;}
114 114 #sidebar a.selected:hover {text-decoration:none;}
115 115 #admin-menu a {line-height:1.7em;}
116 116 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
117 117
118 118 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
119 119 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
120 120
121 121 a#toggle-completed-versions {color:#999;}
122 122 /***** Tables *****/
123 123 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
124 124 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
125 125 table.list td { vertical-align: top; }
126 126 table.list td.id { width: 2%; text-align: center;}
127 127 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
128 128 table.list td.checkbox input {padding:0px;}
129 129 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
130 130 table.list td.buttons a { padding-right: 0.6em; }
131 131 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
132 132
133 133 tr.project td.name a { white-space:nowrap; }
134 134 tr.project.closed, tr.project.archived { color: #aaa; }
135 135 tr.project.closed a, tr.project.archived a { color: #aaa; }
136 136
137 137 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
138 138 tr.project.idnt-1 td.name {padding-left: 0.5em;}
139 139 tr.project.idnt-2 td.name {padding-left: 2em;}
140 140 tr.project.idnt-3 td.name {padding-left: 3.5em;}
141 141 tr.project.idnt-4 td.name {padding-left: 5em;}
142 142 tr.project.idnt-5 td.name {padding-left: 6.5em;}
143 143 tr.project.idnt-6 td.name {padding-left: 8em;}
144 144 tr.project.idnt-7 td.name {padding-left: 9.5em;}
145 145 tr.project.idnt-8 td.name {padding-left: 11em;}
146 146 tr.project.idnt-9 td.name {padding-left: 12.5em;}
147 147
148 148 tr.issue { text-align: center; white-space: nowrap; }
149 149 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text { white-space: normal; }
150 150 tr.issue td.subject { text-align: left; }
151 151 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
152 152
153 153 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
154 154 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
155 155 tr.issue.idnt-2 td.subject {padding-left: 2em;}
156 156 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
157 157 tr.issue.idnt-4 td.subject {padding-left: 5em;}
158 158 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
159 159 tr.issue.idnt-6 td.subject {padding-left: 8em;}
160 160 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
161 161 tr.issue.idnt-8 td.subject {padding-left: 11em;}
162 162 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
163 163
164 164 tr.entry { border: 1px solid #f8f8f8; }
165 165 tr.entry td { white-space: nowrap; }
166 166 tr.entry td.filename { width: 30%; }
167 167 tr.entry td.filename_no_report { width: 70%; }
168 168 tr.entry td.size { text-align: right; font-size: 90%; }
169 169 tr.entry td.revision, tr.entry td.author { text-align: center; }
170 170 tr.entry td.age { text-align: right; }
171 171 tr.entry.file td.filename a { margin-left: 16px; }
172 172 tr.entry.file td.filename_no_report a { margin-left: 16px; }
173 173
174 174 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
175 175 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
176 176
177 177 tr.changeset { height: 20px }
178 178 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
179 179 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
180 180 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
181 181 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
182 182
183 183 table.files tr.file td { text-align: center; }
184 184 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
185 185 table.files tr.file td.digest { font-size: 80%; }
186 186
187 187 table.members td.roles, table.memberships td.roles { width: 45%; }
188 188
189 189 tr.message { height: 2.6em; }
190 190 tr.message td.subject { padding-left: 20px; }
191 191 tr.message td.created_on { white-space: nowrap; }
192 192 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
193 193 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
194 194 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
195 195
196 196 tr.version.closed, tr.version.closed a { color: #999; }
197 197 tr.version td.name { padding-left: 20px; }
198 198 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
199 199 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
200 200
201 201 tr.user td { width:13%; }
202 202 tr.user td.email { width:18%; }
203 203 tr.user td { white-space: nowrap; }
204 204 tr.user.locked, tr.user.registered { color: #aaa; }
205 205 tr.user.locked a, tr.user.registered a { color: #aaa; }
206 206
207 207 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
208 208
209 209 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
210 210
211 211 tr.time-entry { text-align: center; white-space: nowrap; }
212 212 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
213 213 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
214 214 td.hours .hours-dec { font-size: 0.9em; }
215 215
216 216 table.plugins td { vertical-align: middle; }
217 217 table.plugins td.configure { text-align: right; padding-right: 1em; }
218 218 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
219 219 table.plugins span.description { display: block; font-size: 0.9em; }
220 220 table.plugins span.url { display: block; font-size: 0.9em; }
221 221
222 222 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
223 223 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
224 224 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
225 225 tr.group:hover a.toggle-all { display:inline;}
226 226 a.toggle-all:hover {text-decoration:none;}
227 227
228 228 table.list tbody tr:hover { background-color:#ffffdd; }
229 229 table.list tbody tr.group:hover { background-color:inherit; }
230 230 table td {padding:2px;}
231 231 table p {margin:0;}
232 232 .odd {background-color:#f6f7f8;}
233 233 .even {background-color: #fff;}
234 234
235 235 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
236 236 a.sort.asc { background-image: url(../images/sort_asc.png); }
237 237 a.sort.desc { background-image: url(../images/sort_desc.png); }
238 238
239 239 table.attributes { width: 100% }
240 240 table.attributes th { vertical-align: top; text-align: left; }
241 241 table.attributes td { vertical-align: top; }
242 242
243 243 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
244 244
245 245 table.query-columns {
246 246 border-collapse: collapse;
247 247 border: 0;
248 248 }
249 249
250 250 table.query-columns td.buttons {
251 251 vertical-align: middle;
252 252 text-align: center;
253 253 }
254 254
255 255 td.center {text-align:center;}
256 256
257 257 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
258 258
259 259 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
260 260 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
261 261 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
262 262 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
263 263
264 264 #watchers ul {margin: 0; padding: 0;}
265 265 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
266 266 #watchers select {width: 95%; display: block;}
267 267 #watchers a.delete {opacity: 0.4;}
268 268 #watchers a.delete:hover {opacity: 1;}
269 269 #watchers img.gravatar {margin: 0 4px 2px 0;}
270 270
271 271 span#watchers_inputs {overflow:auto; display:block;}
272 272 span.search_for_watchers {display:block;}
273 273 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
274 274 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
275 275
276 276
277 277 .highlight { background-color: #FCFD8D;}
278 278 .highlight.token-1 { background-color: #faa;}
279 279 .highlight.token-2 { background-color: #afa;}
280 280 .highlight.token-3 { background-color: #aaf;}
281 281
282 282 .box{
283 283 padding:6px;
284 284 margin-bottom: 10px;
285 285 background-color:#f6f6f6;
286 286 color:#505050;
287 287 line-height:1.5em;
288 288 border: 1px solid #e4e4e4;
289 289 }
290 290
291 291 div.square {
292 292 border: 1px solid #999;
293 293 float: left;
294 294 margin: .3em .4em 0 .4em;
295 295 overflow: hidden;
296 296 width: .6em; height: .6em;
297 297 }
298 298 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
299 299 .contextual input, .contextual select {font-size:0.9em;}
300 300 .message .contextual { margin-top: 0; }
301 301
302 302 .splitcontent {overflow:auto;}
303 303 .splitcontentleft{float:left; width:49%;}
304 304 .splitcontentright{float:right; width:49%;}
305 305 form {display: inline;}
306 306 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
307 307 fieldset {border: 1px solid #e4e4e4; margin:0;}
308 308 legend {color: #484848;}
309 309 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
310 310 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
311 311 blockquote blockquote { margin-left: 0;}
312 312 acronym { border-bottom: 1px dotted; cursor: help; }
313 313 textarea.wiki-edit { width: 99%; }
314 314 li p {margin-top: 0;}
315 315 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
316 316 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
317 317 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
318 318 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
319 319
320 320 div.issue div.subject div div { padding-left: 16px; }
321 321 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
322 322 div.issue div.subject>div>p { margin-top: 0.5em; }
323 323 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
324 324 div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px; -moz-border-radius: 2px;}
325 325 div.issue .next-prev-links {color:#999;}
326 326 div.issue table.attributes th {width:22%;}
327 327 div.issue table.attributes td {width:28%;}
328 328
329 329 #issue_tree table.issues, #relations table.issues { border: 0; }
330 330 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
331 331 #relations td.buttons {padding:0;}
332 332
333 333 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
334 334 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
335 335 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
336 336
337 337 fieldset#date-range p { margin: 2px 0 2px 0; }
338 338 fieldset#filters table { border-collapse: collapse; }
339 339 fieldset#filters table td { padding: 0; vertical-align: middle; }
340 340 fieldset#filters tr.filter { height: 2em; }
341 341 fieldset#filters td.field { width:200px; }
342 342 fieldset#filters td.operator { width:170px; }
343 343 fieldset#filters td.values { white-space:nowrap; }
344 344 fieldset#filters td.values select {min-width:130px;}
345 345 fieldset#filters td.values img { vertical-align: middle; margin-left:1px; }
346 346 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
347 347 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
348 348
349 349 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
350 350 div#issue-changesets div.changeset { padding: 4px;}
351 351 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
352 352 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
353 353
354 354 .journal ul.details img {margin:0 0 -3px 4px;}
355 355
356 356 div#activity dl, #search-results { margin-left: 2em; }
357 357 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
358 358 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
359 359 div#activity dt.me .time { border-bottom: 1px solid #999; }
360 360 div#activity dt .time { color: #777; font-size: 80%; }
361 361 div#activity dd .description, #search-results dd .description { font-style: italic; }
362 362 div#activity span.project:after, #search-results span.project:after { content: " -"; }
363 363 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
364 364
365 365 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
366 366
367 367 div#search-results-counts {float:right;}
368 368 div#search-results-counts ul { margin-top: 0.5em; }
369 369 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
370 370
371 371 dt.issue { background-image: url(../images/ticket.png); }
372 372 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
373 373 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
374 374 dt.issue-note { background-image: url(../images/ticket_note.png); }
375 375 dt.changeset { background-image: url(../images/changeset.png); }
376 376 dt.news { background-image: url(../images/news.png); }
377 377 dt.message { background-image: url(../images/message.png); }
378 378 dt.reply { background-image: url(../images/comments.png); }
379 379 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
380 380 dt.attachment { background-image: url(../images/attachment.png); }
381 381 dt.document { background-image: url(../images/document.png); }
382 382 dt.project { background-image: url(../images/projects.png); }
383 383 dt.time-entry { background-image: url(../images/time.png); }
384 384
385 385 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
386 386
387 387 div#roadmap .related-issues { margin-bottom: 1em; }
388 388 div#roadmap .related-issues td.checkbox { display: none; }
389 389 div#roadmap .wiki h1:first-child { display: none; }
390 390 div#roadmap .wiki h1 { font-size: 120%; }
391 391 div#roadmap .wiki h2 { font-size: 110%; }
392 392 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
393 393
394 394 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
395 395 div#version-summary fieldset { margin-bottom: 1em; }
396 396 div#version-summary fieldset.time-tracking table { width:100%; }
397 397 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
398 398
399 399 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
400 400 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
401 401 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
402 402 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
403 403 table#time-report .hours-dec { font-size: 0.9em; }
404 404
405 405 div.wiki-page .contextual a {opacity: 0.4}
406 406 div.wiki-page .contextual a:hover {opacity: 1}
407 407
408 408 form .attributes select { width: 60%; }
409 409 input#issue_subject { width: 99%; }
410 410 select#issue_done_ratio { width: 95px; }
411 411
412 412 ul.projects { margin: 0; padding-left: 1em; }
413 413 ul.projects.root { margin: 0; padding: 0; }
414 414 ul.projects ul.projects { border-left: 3px solid #e0e0e0; }
415 415 ul.projects li.root { list-style-type:none; margin-bottom: 1em; }
416 416 ul.projects li.child { list-style-type:none; margin-top: 1em;}
417 417 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
418 418 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
419 419
420 420 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
421 421 #tracker_project_ids li { list-style-type:none; }
422 422
423 423 #related-issues li img {vertical-align:middle;}
424 424
425 425 ul.properties {padding:0; font-size: 0.9em; color: #777;}
426 426 ul.properties li {list-style-type:none;}
427 427 ul.properties li span {font-style:italic;}
428 428
429 429 .total-hours { font-size: 110%; font-weight: bold; }
430 430 .total-hours span.hours-int { font-size: 120%; }
431 431
432 432 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
433 433 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
434 434
435 435 #workflow_copy_form select { width: 200px; }
436 table.fields_permissions select {font-size:90%}
437 table.fields_permissions td.readonly {background:#ddd;}
438 table.fields_permissions td.required {background:#d88;}
436 439
437 440 textarea#custom_field_possible_values {width: 99%}
438 441 input#content_comments {width: 99%}
439 442
440 443 .pagination {font-size: 90%}
441 444 p.pagination {margin-top:8px;}
442 445
443 446 /***** Tabular forms ******/
444 447 .tabular p{
445 448 margin: 0;
446 449 padding: 3px 0 3px 0;
447 450 padding-left: 180px; /* width of left column containing the label elements */
448 451 min-height: 1.8em;
449 452 clear:left;
450 453 }
451 454
452 455 html>body .tabular p {overflow:hidden;}
453 456
454 457 .tabular label{
455 458 font-weight: bold;
456 459 float: left;
457 460 text-align: right;
458 461 /* width of left column */
459 462 margin-left: -180px;
460 463 /* width of labels. Should be smaller than left column to create some right margin */
461 464 width: 175px;
462 465 }
463 466
464 467 .tabular label.floating{
465 468 font-weight: normal;
466 469 margin-left: 0px;
467 470 text-align: left;
468 471 width: 270px;
469 472 }
470 473
471 474 .tabular label.block{
472 475 font-weight: normal;
473 476 margin-left: 0px !important;
474 477 text-align: left;
475 478 float: none;
476 479 display: block;
477 480 width: auto;
478 481 }
479 482
480 483 .tabular label.inline{
481 484 float:none;
482 485 margin-left: 5px !important;
483 486 width: auto;
484 487 }
485 488
486 489 label.no-css {
487 490 font-weight: inherit;
488 491 float:none;
489 492 text-align:left;
490 493 margin-left:0px;
491 494 width:auto;
492 495 }
493 496 input#time_entry_comments { width: 90%;}
494 497
495 498 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
496 499
497 500 .tabular.settings p{ padding-left: 300px; }
498 501 .tabular.settings label{ margin-left: -300px; width: 295px; }
499 502 .tabular.settings textarea { width: 99%; }
500 503
501 504 .settings.enabled_scm table {width:100%}
502 505 .settings.enabled_scm td.scm_name{ font-weight: bold; }
503 506
504 507 fieldset.settings label { display: block; }
505 508 fieldset#notified_events .parent { padding-left: 20px; }
506 509
507 .required {color: #bb0000;}
510 span.required {color: #bb0000;}
508 511 .summary {font-style: italic;}
509 512
510 513 #attachments_fields input.description {margin-left: 8px; width:340px;}
511 514 #attachments_fields span {display:block; white-space:nowrap;}
512 515 #attachments_fields img {vertical-align: middle;}
513 516
514 517 div.attachments { margin-top: 12px; }
515 518 div.attachments p { margin:4px 0 2px 0; }
516 519 div.attachments img { vertical-align: middle; }
517 520 div.attachments span.author { font-size: 0.9em; color: #888; }
518 521
519 522 div.thumbnails {margin-top:0.6em;}
520 523 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
521 524 div.thumbnails img {margin: 3px;}
522 525
523 526 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
524 527 .other-formats span + span:before { content: "| "; }
525 528
526 529 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
527 530
528 531 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
529 532 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
530 533
531 534 textarea.text_cf {width:90%;}
532 535
533 536 /* Project members tab */
534 537 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
535 538 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
536 539 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
537 540 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
538 541 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
539 542 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
540 543
541 544 #users_for_watcher {height: 200px; overflow:auto;}
542 545 #users_for_watcher label {display: block;}
543 546
544 547 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
545 548
546 549 input#principal_search, input#user_search {width:100%}
547 550 input#principal_search, input#user_search {
548 551 background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px;
549 552 border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%;
550 553 }
551 554 input#principal_search.ajax-loading, input#user_search.ajax-loading {
552 555 background-image: url(../images/loading.gif);
553 556 }
554 557
555 558 * html div#tab-content-members fieldset div { height: 450px; }
556 559
557 560 /***** Flash & error messages ****/
558 561 #errorExplanation, div.flash, .nodata, .warning, .conflict {
559 562 padding: 4px 4px 4px 30px;
560 563 margin-bottom: 12px;
561 564 font-size: 1.1em;
562 565 border: 2px solid;
563 566 }
564 567
565 568 div.flash {margin-top: 8px;}
566 569
567 570 div.flash.error, #errorExplanation {
568 571 background: url(../images/exclamation.png) 8px 50% no-repeat;
569 572 background-color: #ffe3e3;
570 573 border-color: #dd0000;
571 574 color: #880000;
572 575 }
573 576
574 577 div.flash.notice {
575 578 background: url(../images/true.png) 8px 5px no-repeat;
576 579 background-color: #dfffdf;
577 580 border-color: #9fcf9f;
578 581 color: #005f00;
579 582 }
580 583
581 584 div.flash.warning, .conflict {
582 585 background: url(../images/warning.png) 8px 5px no-repeat;
583 586 background-color: #FFEBC1;
584 587 border-color: #FDBF3B;
585 588 color: #A6750C;
586 589 text-align: left;
587 590 }
588 591
589 592 .nodata, .warning {
590 593 text-align: center;
591 594 background-color: #FFEBC1;
592 595 border-color: #FDBF3B;
593 596 color: #A6750C;
594 597 }
595 598
596 599 #errorExplanation ul { font-size: 0.9em;}
597 600 #errorExplanation h2, #errorExplanation p { display: none; }
598 601
599 602 .conflict-details {font-size:80%;}
600 603
601 604 /***** Ajax indicator ******/
602 605 #ajax-indicator {
603 606 position: absolute; /* fixed not supported by IE */
604 607 background-color:#eee;
605 608 border: 1px solid #bbb;
606 609 top:35%;
607 610 left:40%;
608 611 width:20%;
609 612 font-weight:bold;
610 613 text-align:center;
611 614 padding:0.6em;
612 615 z-index:100;
613 616 opacity: 0.5;
614 617 }
615 618
616 619 html>body #ajax-indicator { position: fixed; }
617 620
618 621 #ajax-indicator span {
619 622 background-position: 0% 40%;
620 623 background-repeat: no-repeat;
621 624 background-image: url(../images/loading.gif);
622 625 padding-left: 26px;
623 626 vertical-align: bottom;
624 627 }
625 628
626 629 /***** Calendar *****/
627 630 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
628 631 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
629 632 table.cal thead th.week-number {width: auto;}
630 633 table.cal tbody tr {height: 100px;}
631 634 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
632 635 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
633 636 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
634 637 table.cal td.odd p.day-num {color: #bbb;}
635 638 table.cal td.today {background:#ffffdd;}
636 639 table.cal td.today p.day-num {font-weight: bold;}
637 640 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
638 641 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
639 642 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
640 643 p.cal.legend span {display:block;}
641 644
642 645 /***** Tooltips ******/
643 646 .tooltip{position:relative;z-index:24;}
644 647 .tooltip:hover{z-index:25;color:#000;}
645 648 .tooltip span.tip{display: none; text-align:left;}
646 649
647 650 div.tooltip:hover span.tip{
648 651 display:block;
649 652 position:absolute;
650 653 top:12px; left:24px; width:270px;
651 654 border:1px solid #555;
652 655 background-color:#fff;
653 656 padding: 4px;
654 657 font-size: 0.8em;
655 658 color:#505050;
656 659 }
657 660
658 661 /***** Progress bar *****/
659 662 table.progress {
660 663 border-collapse: collapse;
661 664 border-spacing: 0pt;
662 665 empty-cells: show;
663 666 text-align: center;
664 667 float:left;
665 668 margin: 1px 6px 1px 0px;
666 669 }
667 670
668 671 table.progress td { height: 1em; }
669 672 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
670 673 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
671 674 table.progress td.todo { background: #eee none repeat scroll 0%; }
672 675 p.pourcent {font-size: 80%;}
673 676 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
674 677
675 678 #roadmap table.progress td { height: 1.2em; }
676 679 /***** Tabs *****/
677 680 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
678 681 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
679 682 #content .tabs ul li {
680 683 float:left;
681 684 list-style-type:none;
682 685 white-space:nowrap;
683 686 margin-right:4px;
684 687 background:#fff;
685 688 position:relative;
686 689 margin-bottom:-1px;
687 690 }
688 691 #content .tabs ul li a{
689 692 display:block;
690 693 font-size: 0.9em;
691 694 text-decoration:none;
692 695 line-height:1.3em;
693 696 padding:4px 6px 4px 6px;
694 697 border: 1px solid #ccc;
695 698 border-bottom: 1px solid #bbbbbb;
696 699 background-color: #f6f6f6;
697 700 color:#999;
698 701 font-weight:bold;
699 702 border-top-left-radius:3px;
700 703 border-top-right-radius:3px;
701 704 }
702 705
703 706 #content .tabs ul li a:hover {
704 707 background-color: #ffffdd;
705 708 text-decoration:none;
706 709 }
707 710
708 711 #content .tabs ul li a.selected {
709 712 background-color: #fff;
710 713 border: 1px solid #bbbbbb;
711 714 border-bottom: 1px solid #fff;
712 715 color:#444;
713 716 }
714 717
715 718 #content .tabs ul li a.selected:hover {
716 719 background-color: #fff;
717 720 }
718 721
719 722 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
720 723
721 724 button.tab-left, button.tab-right {
722 725 font-size: 0.9em;
723 726 cursor: pointer;
724 727 height:24px;
725 728 border: 1px solid #ccc;
726 729 border-bottom: 1px solid #bbbbbb;
727 730 position:absolute;
728 731 padding:4px;
729 732 width: 20px;
730 733 bottom: -1px;
731 734 }
732 735
733 736 button.tab-left {
734 737 right: 20px;
735 738 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
736 739 border-top-left-radius:3px;
737 740 }
738 741
739 742 button.tab-right {
740 743 right: 0;
741 744 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
742 745 border-top-right-radius:3px;
743 746 }
744 747
745 748 /***** Auto-complete *****/
746 749 div.autocomplete {
747 750 position:absolute;
748 751 width:400px;
749 752 margin:0;
750 753 padding:0;
751 754 }
752 755 div.autocomplete ul {
753 756 list-style-type:none;
754 757 margin:0;
755 758 padding:0;
756 759 }
757 760 div.autocomplete ul li {
758 761 list-style-type:none;
759 762 display:block;
760 763 margin:-1px 0 0 0;
761 764 padding:2px;
762 765 cursor:pointer;
763 766 font-size: 90%;
764 767 border: 1px solid #ccc;
765 768 border-left: 1px solid #ccc;
766 769 border-right: 1px solid #ccc;
767 770 background-color:white;
768 771 }
769 772 div.autocomplete ul li.selected { background-color: #ffb;}
770 773 div.autocomplete ul li span.informal {
771 774 font-size: 80%;
772 775 color: #aaa;
773 776 }
774 777
775 778 #parent_issue_candidates ul li {width: 500px;}
776 779 #related_issue_candidates ul li {width: 500px;}
777 780
778 781 /***** Diff *****/
779 782 .diff_out { background: #fcc; }
780 783 .diff_out span { background: #faa; }
781 784 .diff_in { background: #cfc; }
782 785 .diff_in span { background: #afa; }
783 786
784 787 .text-diff {
785 788 padding: 1em;
786 789 background-color:#f6f6f6;
787 790 color:#505050;
788 791 border: 1px solid #e4e4e4;
789 792 }
790 793
791 794 /***** Wiki *****/
792 795 div.wiki table {
793 796 border-collapse: collapse;
794 797 margin-bottom: 1em;
795 798 }
796 799
797 800 div.wiki table, div.wiki td, div.wiki th {
798 801 border: 1px solid #bbb;
799 802 padding: 4px;
800 803 }
801 804
802 805 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
803 806
804 807 div.wiki .external {
805 808 background-position: 0% 60%;
806 809 background-repeat: no-repeat;
807 810 padding-left: 12px;
808 811 background-image: url(../images/external.png);
809 812 }
810 813
811 814 div.wiki a.new {
812 815 color: #b73535;
813 816 }
814 817
815 818 div.wiki ul, div.wiki ol {margin-bottom:1em;}
816 819
817 820 div.wiki pre {
818 821 margin: 1em 1em 1em 1.6em;
819 822 padding: 2px 2px 2px 0;
820 823 background-color: #fafafa;
821 824 border: 1px solid #dadada;
822 825 width:auto;
823 826 overflow-x: auto;
824 827 overflow-y: hidden;
825 828 }
826 829
827 830 div.wiki ul.toc {
828 831 background-color: #ffffdd;
829 832 border: 1px solid #e4e4e4;
830 833 padding: 4px;
831 834 line-height: 1.2em;
832 835 margin-bottom: 12px;
833 836 margin-right: 12px;
834 837 margin-left: 0;
835 838 display: table
836 839 }
837 840 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
838 841
839 842 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
840 843 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
841 844 div.wiki ul.toc ul { margin: 0; padding: 0; }
842 845 div.wiki ul.toc li { list-style-type:none; margin: 0;}
843 846 div.wiki ul.toc li li { margin-left: 1.5em; }
844 847 div.wiki ul.toc li li li { font-size: 0.8em; }
845 848
846 849 div.wiki ul.toc a {
847 850 font-size: 0.9em;
848 851 font-weight: normal;
849 852 text-decoration: none;
850 853 color: #606060;
851 854 }
852 855 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
853 856
854 857 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
855 858 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
856 859 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
857 860
858 861 div.wiki img { vertical-align: middle; }
859 862
860 863 /***** My page layout *****/
861 864 .block-receiver {
862 865 border:1px dashed #c0c0c0;
863 866 margin-bottom: 20px;
864 867 padding: 15px 0 15px 0;
865 868 }
866 869
867 870 .mypage-box {
868 871 margin:0 0 20px 0;
869 872 color:#505050;
870 873 line-height:1.5em;
871 874 }
872 875
873 876 .handle {
874 877 cursor: move;
875 878 }
876 879
877 880 a.close-icon {
878 881 display:block;
879 882 margin-top:3px;
880 883 overflow:hidden;
881 884 width:12px;
882 885 height:12px;
883 886 background-repeat: no-repeat;
884 887 cursor:pointer;
885 888 background-image:url('../images/close.png');
886 889 }
887 890
888 891 a.close-icon:hover {
889 892 background-image:url('../images/close_hl.png');
890 893 }
891 894
892 895 /***** Gantt chart *****/
893 896 .gantt_hdr {
894 897 position:absolute;
895 898 top:0;
896 899 height:16px;
897 900 border-top: 1px solid #c0c0c0;
898 901 border-bottom: 1px solid #c0c0c0;
899 902 border-right: 1px solid #c0c0c0;
900 903 text-align: center;
901 904 overflow: hidden;
902 905 }
903 906
904 907 .gantt_subjects { font-size: 0.8em; }
905 908 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
906 909
907 910 .task {
908 911 position: absolute;
909 912 height:8px;
910 913 font-size:0.8em;
911 914 color:#888;
912 915 padding:0;
913 916 margin:0;
914 917 line-height:16px;
915 918 white-space:nowrap;
916 919 }
917 920
918 921 .task.label {width:100%;}
919 922 .task.label.project, .task.label.version { font-weight: bold; }
920 923
921 924 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
922 925 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
923 926 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
924 927
925 928 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
926 929 .task_late.parent, .task_done.parent { height: 3px;}
927 930 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
928 931 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
929 932
930 933 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
931 934 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
932 935 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
933 936 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
934 937
935 938 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
936 939 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
937 940 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
938 941 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
939 942
940 943 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
941 944 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
942 945
943 946 /***** Icons *****/
944 947 .icon {
945 948 background-position: 0% 50%;
946 949 background-repeat: no-repeat;
947 950 padding-left: 20px;
948 951 padding-top: 2px;
949 952 padding-bottom: 3px;
950 953 }
951 954
952 955 .icon-add { background-image: url(../images/add.png); }
953 956 .icon-edit { background-image: url(../images/edit.png); }
954 957 .icon-copy { background-image: url(../images/copy.png); }
955 958 .icon-duplicate { background-image: url(../images/duplicate.png); }
956 959 .icon-del { background-image: url(../images/delete.png); }
957 960 .icon-move { background-image: url(../images/move.png); }
958 961 .icon-save { background-image: url(../images/save.png); }
959 962 .icon-cancel { background-image: url(../images/cancel.png); }
960 963 .icon-multiple { background-image: url(../images/table_multiple.png); }
961 964 .icon-folder { background-image: url(../images/folder.png); }
962 965 .open .icon-folder { background-image: url(../images/folder_open.png); }
963 966 .icon-package { background-image: url(../images/package.png); }
964 967 .icon-user { background-image: url(../images/user.png); }
965 968 .icon-projects { background-image: url(../images/projects.png); }
966 969 .icon-help { background-image: url(../images/help.png); }
967 970 .icon-attachment { background-image: url(../images/attachment.png); }
968 971 .icon-history { background-image: url(../images/history.png); }
969 972 .icon-time { background-image: url(../images/time.png); }
970 973 .icon-time-add { background-image: url(../images/time_add.png); }
971 974 .icon-stats { background-image: url(../images/stats.png); }
972 975 .icon-warning { background-image: url(../images/warning.png); }
973 976 .icon-fav { background-image: url(../images/fav.png); }
974 977 .icon-fav-off { background-image: url(../images/fav_off.png); }
975 978 .icon-reload { background-image: url(../images/reload.png); }
976 979 .icon-lock { background-image: url(../images/locked.png); }
977 980 .icon-unlock { background-image: url(../images/unlock.png); }
978 981 .icon-checked { background-image: url(../images/true.png); }
979 982 .icon-details { background-image: url(../images/zoom_in.png); }
980 983 .icon-report { background-image: url(../images/report.png); }
981 984 .icon-comment { background-image: url(../images/comment.png); }
982 985 .icon-summary { background-image: url(../images/lightning.png); }
983 986 .icon-server-authentication { background-image: url(../images/server_key.png); }
984 987 .icon-issue { background-image: url(../images/ticket.png); }
985 988 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
986 989 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
987 990 .icon-passwd { background-image: url(../images/textfield_key.png); }
988 991 .icon-test { background-image: url(../images/bullet_go.png); }
989 992
990 993 .icon-file { background-image: url(../images/files/default.png); }
991 994 .icon-file.text-plain { background-image: url(../images/files/text.png); }
992 995 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
993 996 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
994 997 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
995 998 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
996 999 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
997 1000 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
998 1001 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
999 1002 .icon-file.text-css { background-image: url(../images/files/css.png); }
1000 1003 .icon-file.text-html { background-image: url(../images/files/html.png); }
1001 1004 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1002 1005 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1003 1006 .icon-file.image-png { background-image: url(../images/files/image.png); }
1004 1007 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1005 1008 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1006 1009 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1007 1010 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1008 1011
1009 1012 img.gravatar {
1010 1013 padding: 2px;
1011 1014 border: solid 1px #d5d5d5;
1012 1015 background: #fff;
1013 1016 vertical-align: middle;
1014 1017 }
1015 1018
1016 1019 div.issue img.gravatar {
1017 1020 float: left;
1018 1021 margin: 0 6px 0 0;
1019 1022 padding: 5px;
1020 1023 }
1021 1024
1022 1025 div.issue table img.gravatar {
1023 1026 height: 14px;
1024 1027 width: 14px;
1025 1028 padding: 2px;
1026 1029 float: left;
1027 1030 margin: 0 0.5em 0 0;
1028 1031 }
1029 1032
1030 1033 h2 img.gravatar {
1031 1034 margin: -2px 4px -4px 0;
1032 1035 }
1033 1036
1034 1037 h3 img.gravatar {
1035 1038 margin: -4px 4px -4px 0;
1036 1039 }
1037 1040
1038 1041 h4 img.gravatar {
1039 1042 margin: -6px 4px -4px 0;
1040 1043 }
1041 1044
1042 1045 td.username img.gravatar {
1043 1046 margin: 0 0.5em 0 0;
1044 1047 vertical-align: top;
1045 1048 }
1046 1049
1047 1050 #activity dt img.gravatar {
1048 1051 float: left;
1049 1052 margin: 0 1em 1em 0;
1050 1053 }
1051 1054
1052 1055 /* Used on 12px Gravatar img tags without the icon background */
1053 1056 .icon-gravatar {
1054 1057 float: left;
1055 1058 margin-right: 4px;
1056 1059 }
1057 1060
1058 1061 #activity dt,
1059 1062 .journal {
1060 1063 clear: left;
1061 1064 }
1062 1065
1063 1066 .journal-link {
1064 1067 float: right;
1065 1068 }
1066 1069
1067 1070 h2 img { vertical-align:middle; }
1068 1071
1069 1072 .hascontextmenu { cursor: context-menu; }
1070 1073
1071 1074 /***** Media print specific styles *****/
1072 1075 @media print {
1073 1076 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1074 1077 #main { background: #fff; }
1075 1078 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1076 1079 #wiki_add_attachment { display:none; }
1077 1080 .hide-when-print { display: none; }
1078 1081 .autoscroll {overflow-x: visible;}
1079 1082 table.list {margin-top:0.5em;}
1080 1083 table.list th, table.list td {border: 1px solid #aaa;}
1081 1084 }
1082 1085
1083 1086 /* Accessibility specific styles */
1084 1087 .hidden-for-sighted {
1085 1088 position:absolute;
1086 1089 left:-10000px;
1087 1090 top:auto;
1088 1091 width:1px;
1089 1092 height:1px;
1090 1093 overflow:hidden;
1091 1094 }
This diff has been collapsed as it changes many lines, (807 lines changed) Show them Hide them
@@ -1,1615 +1,1884
1 1 ---
2 workflows_189:
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 workflows_001:
8 type: WorkflowTransition
9 WorkflowTransitions_001:
9 10 new_status_id: 2
10 11 role_id: 1
11 12 old_status_id: 1
12 13 id: 1
13 14 tracker_id: 1
14 workflows_002:
15 type: WorkflowTransition
16 WorkflowTransitions_002:
15 17 new_status_id: 3
16 18 role_id: 1
17 19 old_status_id: 1
18 20 id: 2
19 21 tracker_id: 1
20 workflows_003:
22 type: WorkflowTransition
23 WorkflowTransitions_003:
21 24 new_status_id: 4
22 25 role_id: 1
23 26 old_status_id: 1
24 27 id: 3
25 28 tracker_id: 1
26 workflows_110:
29 type: WorkflowTransition
30 WorkflowTransitions_110:
27 31 new_status_id: 6
28 32 role_id: 1
29 33 old_status_id: 4
30 34 id: 110
31 35 tracker_id: 2
32 workflows_004:
36 type: WorkflowTransition
37 WorkflowTransitions_004:
33 38 new_status_id: 5
34 39 role_id: 1
35 40 old_status_id: 1
36 41 id: 4
37 42 tracker_id: 1
38 workflows_030:
43 type: WorkflowTransition
44 WorkflowTransitions_030:
39 45 new_status_id: 5
40 46 role_id: 1
41 47 old_status_id: 6
42 48 id: 30
43 49 tracker_id: 1
44 workflows_111:
50 type: WorkflowTransition
51 WorkflowTransitions_111:
45 52 new_status_id: 1
46 53 role_id: 1
47 54 old_status_id: 5
48 55 id: 111
49 56 tracker_id: 2
50 workflows_005:
57 type: WorkflowTransition
58 WorkflowTransitions_005:
51 59 new_status_id: 6
52 60 role_id: 1
53 61 old_status_id: 1
54 62 id: 5
55 63 tracker_id: 1
56 workflows_031:
64 type: WorkflowTransition
65 WorkflowTransitions_031:
57 66 new_status_id: 2
58 67 role_id: 2
59 68 old_status_id: 1
60 69 id: 31
61 70 tracker_id: 1
62 workflows_112:
71 type: WorkflowTransition
72 WorkflowTransitions_112:
63 73 new_status_id: 2
64 74 role_id: 1
65 75 old_status_id: 5
66 76 id: 112
67 77 tracker_id: 2
68 workflows_006:
78 type: WorkflowTransition
79 WorkflowTransitions_006:
69 80 new_status_id: 1
70 81 role_id: 1
71 82 old_status_id: 2
72 83 id: 6
73 84 tracker_id: 1
74 workflows_032:
85 type: WorkflowTransition
86 WorkflowTransitions_032:
75 87 new_status_id: 3
76 88 role_id: 2
77 89 old_status_id: 1
78 90 id: 32
79 91 tracker_id: 1
80 workflows_113:
92 type: WorkflowTransition
93 WorkflowTransitions_113:
81 94 new_status_id: 3
82 95 role_id: 1
83 96 old_status_id: 5
84 97 id: 113
85 98 tracker_id: 2
86 workflows_220:
99 type: WorkflowTransition
100 WorkflowTransitions_220:
87 101 new_status_id: 6
88 102 role_id: 2
89 103 old_status_id: 2
90 104 id: 220
91 105 tracker_id: 3
92 workflows_007:
106 type: WorkflowTransition
107 WorkflowTransitions_007:
93 108 new_status_id: 3
94 109 role_id: 1
95 110 old_status_id: 2
96 111 id: 7
97 112 tracker_id: 1
98 workflows_033:
113 type: WorkflowTransition
114 WorkflowTransitions_033:
99 115 new_status_id: 4
100 116 role_id: 2
101 117 old_status_id: 1
102 118 id: 33
103 119 tracker_id: 1
104 workflows_060:
120 type: WorkflowTransition
121 WorkflowTransitions_060:
105 122 new_status_id: 5
106 123 role_id: 2
107 124 old_status_id: 6
108 125 id: 60
109 126 tracker_id: 1
110 workflows_114:
127 type: WorkflowTransition
128 WorkflowTransitions_114:
111 129 new_status_id: 4
112 130 role_id: 1
113 131 old_status_id: 5
114 132 id: 114
115 133 tracker_id: 2
116 workflows_140:
134 type: WorkflowTransition
135 WorkflowTransitions_140:
117 136 new_status_id: 6
118 137 role_id: 2
119 138 old_status_id: 4
120 139 id: 140
121 140 tracker_id: 2
122 workflows_221:
141 type: WorkflowTransition
142 WorkflowTransitions_221:
123 143 new_status_id: 1
124 144 role_id: 2
125 145 old_status_id: 3
126 146 id: 221
127 147 tracker_id: 3
128 workflows_008:
148 type: WorkflowTransition
149 WorkflowTransitions_008:
129 150 new_status_id: 4
130 151 role_id: 1
131 152 old_status_id: 2
132 153 id: 8
133 154 tracker_id: 1
134 workflows_034:
155 type: WorkflowTransition
156 WorkflowTransitions_034:
135 157 new_status_id: 5
136 158 role_id: 2
137 159 old_status_id: 1
138 160 id: 34
139 161 tracker_id: 1
140 workflows_115:
162 type: WorkflowTransition
163 WorkflowTransitions_115:
141 164 new_status_id: 6
142 165 role_id: 1
143 166 old_status_id: 5
144 167 id: 115
145 168 tracker_id: 2
146 workflows_141:
169 type: WorkflowTransition
170 WorkflowTransitions_141:
147 171 new_status_id: 1
148 172 role_id: 2
149 173 old_status_id: 5
150 174 id: 141
151 175 tracker_id: 2
152 workflows_222:
176 type: WorkflowTransition
177 WorkflowTransitions_222:
153 178 new_status_id: 2
154 179 role_id: 2
155 180 old_status_id: 3
156 181 id: 222
157 182 tracker_id: 3
158 workflows_223:
183 type: WorkflowTransition
184 WorkflowTransitions_223:
159 185 new_status_id: 4
160 186 role_id: 2
161 187 old_status_id: 3
162 188 id: 223
163 189 tracker_id: 3
164 workflows_009:
190 type: WorkflowTransition
191 WorkflowTransitions_009:
165 192 new_status_id: 5
166 193 role_id: 1
167 194 old_status_id: 2
168 195 id: 9
169 196 tracker_id: 1
170 workflows_035:
197 type: WorkflowTransition
198 WorkflowTransitions_035:
171 199 new_status_id: 6
172 200 role_id: 2
173 201 old_status_id: 1
174 202 id: 35
175 203 tracker_id: 1
176 workflows_061:
204 type: WorkflowTransition
205 WorkflowTransitions_061:
177 206 new_status_id: 2
178 207 role_id: 3
179 208 old_status_id: 1
180 209 id: 61
181 210 tracker_id: 1
182 workflows_116:
211 type: WorkflowTransition
212 WorkflowTransitions_116:
183 213 new_status_id: 1
184 214 role_id: 1
185 215 old_status_id: 6
186 216 id: 116
187 217 tracker_id: 2
188 workflows_142:
218 type: WorkflowTransition
219 WorkflowTransitions_142:
189 220 new_status_id: 2
190 221 role_id: 2
191 222 old_status_id: 5
192 223 id: 142
193 224 tracker_id: 2
194 workflows_250:
225 type: WorkflowTransition
226 WorkflowTransitions_250:
195 227 new_status_id: 6
196 228 role_id: 3
197 229 old_status_id: 2
198 230 id: 250
199 231 tracker_id: 3
200 workflows_224:
232 type: WorkflowTransition
233 WorkflowTransitions_224:
201 234 new_status_id: 5
202 235 role_id: 2
203 236 old_status_id: 3
204 237 id: 224
205 238 tracker_id: 3
206 workflows_036:
239 type: WorkflowTransition
240 WorkflowTransitions_036:
207 241 new_status_id: 1
208 242 role_id: 2
209 243 old_status_id: 2
210 244 id: 36
211 245 tracker_id: 1
212 workflows_062:
246 type: WorkflowTransition
247 WorkflowTransitions_062:
213 248 new_status_id: 3
214 249 role_id: 3
215 250 old_status_id: 1
216 251 id: 62
217 252 tracker_id: 1
218 workflows_117:
253 type: WorkflowTransition
254 WorkflowTransitions_117:
219 255 new_status_id: 2
220 256 role_id: 1
221 257 old_status_id: 6
222 258 id: 117
223 259 tracker_id: 2
224 workflows_143:
260 type: WorkflowTransition
261 WorkflowTransitions_143:
225 262 new_status_id: 3
226 263 role_id: 2
227 264 old_status_id: 5
228 265 id: 143
229 266 tracker_id: 2
230 workflows_170:
267 type: WorkflowTransition
268 WorkflowTransitions_170:
231 269 new_status_id: 6
232 270 role_id: 3
233 271 old_status_id: 4
234 272 id: 170
235 273 tracker_id: 2
236 workflows_251:
274 type: WorkflowTransition
275 WorkflowTransitions_251:
237 276 new_status_id: 1
238 277 role_id: 3
239 278 old_status_id: 3
240 279 id: 251
241 280 tracker_id: 3
242 workflows_225:
281 type: WorkflowTransition
282 WorkflowTransitions_225:
243 283 new_status_id: 6
244 284 role_id: 2
245 285 old_status_id: 3
246 286 id: 225
247 287 tracker_id: 3
248 workflows_063:
288 type: WorkflowTransition
289 WorkflowTransitions_063:
249 290 new_status_id: 4
250 291 role_id: 3
251 292 old_status_id: 1
252 293 id: 63
253 294 tracker_id: 1
254 workflows_090:
295 type: WorkflowTransition
296 WorkflowTransitions_090:
255 297 new_status_id: 5
256 298 role_id: 3
257 299 old_status_id: 6
258 300 id: 90
259 301 tracker_id: 1
260 workflows_118:
302 type: WorkflowTransition
303 WorkflowTransitions_118:
261 304 new_status_id: 3
262 305 role_id: 1
263 306 old_status_id: 6
264 307 id: 118
265 308 tracker_id: 2
266 workflows_144:
309 type: WorkflowTransition
310 WorkflowTransitions_144:
267 311 new_status_id: 4
268 312 role_id: 2
269 313 old_status_id: 5
270 314 id: 144
271 315 tracker_id: 2
272 workflows_252:
316 type: WorkflowTransition
317 WorkflowTransitions_252:
273 318 new_status_id: 2
274 319 role_id: 3
275 320 old_status_id: 3
276 321 id: 252
277 322 tracker_id: 3
278 workflows_226:
323 type: WorkflowTransition
324 WorkflowTransitions_226:
279 325 new_status_id: 1
280 326 role_id: 2
281 327 old_status_id: 4
282 328 id: 226
283 329 tracker_id: 3
284 workflows_038:
330 type: WorkflowTransition
331 WorkflowTransitions_038:
285 332 new_status_id: 4
286 333 role_id: 2
287 334 old_status_id: 2
288 335 id: 38
289 336 tracker_id: 1
290 workflows_064:
337 type: WorkflowTransition
338 WorkflowTransitions_064:
291 339 new_status_id: 5
292 340 role_id: 3
293 341 old_status_id: 1
294 342 id: 64
295 343 tracker_id: 1
296 workflows_091:
344 type: WorkflowTransition
345 WorkflowTransitions_091:
297 346 new_status_id: 2
298 347 role_id: 1
299 348 old_status_id: 1
300 349 id: 91
301 350 tracker_id: 2
302 workflows_119:
351 type: WorkflowTransition
352 WorkflowTransitions_119:
303 353 new_status_id: 4
304 354 role_id: 1
305 355 old_status_id: 6
306 356 id: 119
307 357 tracker_id: 2
308 workflows_145:
358 type: WorkflowTransition
359 WorkflowTransitions_145:
309 360 new_status_id: 6
310 361 role_id: 2
311 362 old_status_id: 5
312 363 id: 145
313 364 tracker_id: 2
314 workflows_171:
365 type: WorkflowTransition
366 WorkflowTransitions_171:
315 367 new_status_id: 1
316 368 role_id: 3
317 369 old_status_id: 5
318 370 id: 171
319 371 tracker_id: 2
320 workflows_253:
372 type: WorkflowTransition
373 WorkflowTransitions_253:
321 374 new_status_id: 4
322 375 role_id: 3
323 376 old_status_id: 3
324 377 id: 253
325 378 tracker_id: 3
326 workflows_227:
379 type: WorkflowTransition
380 WorkflowTransitions_227:
327 381 new_status_id: 2
328 382 role_id: 2
329 383 old_status_id: 4
330 384 id: 227
331 385 tracker_id: 3
332 workflows_039:
386 type: WorkflowTransition
387 WorkflowTransitions_039:
333 388 new_status_id: 5
334 389 role_id: 2
335 390 old_status_id: 2
336 391 id: 39
337 392 tracker_id: 1
338 workflows_065:
393 type: WorkflowTransition
394 WorkflowTransitions_065:
339 395 new_status_id: 6
340 396 role_id: 3
341 397 old_status_id: 1
342 398 id: 65
343 399 tracker_id: 1
344 workflows_092:
400 type: WorkflowTransition
401 WorkflowTransitions_092:
345 402 new_status_id: 3
346 403 role_id: 1
347 404 old_status_id: 1
348 405 id: 92
349 406 tracker_id: 2
350 workflows_146:
407 type: WorkflowTransition
408 WorkflowTransitions_146:
351 409 new_status_id: 1
352 410 role_id: 2
353 411 old_status_id: 6
354 412 id: 146
355 413 tracker_id: 2
356 workflows_172:
414 type: WorkflowTransition
415 WorkflowTransitions_172:
357 416 new_status_id: 2
358 417 role_id: 3
359 418 old_status_id: 5
360 419 id: 172
361 420 tracker_id: 2
362 workflows_254:
421 type: WorkflowTransition
422 WorkflowTransitions_254:
363 423 new_status_id: 5
364 424 role_id: 3
365 425 old_status_id: 3
366 426 id: 254
367 427 tracker_id: 3
368 workflows_228:
428 type: WorkflowTransition
429 WorkflowTransitions_228:
369 430 new_status_id: 3
370 431 role_id: 2
371 432 old_status_id: 4
372 433 id: 228
373 434 tracker_id: 3
374 workflows_066:
435 type: WorkflowTransition
436 WorkflowTransitions_066:
375 437 new_status_id: 1
376 438 role_id: 3
377 439 old_status_id: 2
378 440 id: 66
379 441 tracker_id: 1
380 workflows_093:
442 type: WorkflowTransition
443 WorkflowTransitions_093:
381 444 new_status_id: 4
382 445 role_id: 1
383 446 old_status_id: 1
384 447 id: 93
385 448 tracker_id: 2
386 workflows_147:
449 type: WorkflowTransition
450 WorkflowTransitions_147:
387 451 new_status_id: 2
388 452 role_id: 2
389 453 old_status_id: 6
390 454 id: 147
391 455 tracker_id: 2
392 workflows_173:
456 type: WorkflowTransition
457 WorkflowTransitions_173:
393 458 new_status_id: 3
394 459 role_id: 3
395 460 old_status_id: 5
396 461 id: 173
397 462 tracker_id: 2
398 workflows_255:
463 type: WorkflowTransition
464 WorkflowTransitions_255:
399 465 new_status_id: 6
400 466 role_id: 3
401 467 old_status_id: 3
402 468 id: 255
403 469 tracker_id: 3
404 workflows_229:
470 type: WorkflowTransition
471 WorkflowTransitions_229:
405 472 new_status_id: 5
406 473 role_id: 2
407 474 old_status_id: 4
408 475 id: 229
409 476 tracker_id: 3
410 workflows_067:
477 type: WorkflowTransition
478 WorkflowTransitions_067:
411 479 new_status_id: 3
412 480 role_id: 3
413 481 old_status_id: 2
414 482 id: 67
415 483 tracker_id: 1
416 workflows_148:
484 type: WorkflowTransition
485 WorkflowTransitions_148:
417 486 new_status_id: 3
418 487 role_id: 2
419 488 old_status_id: 6
420 489 id: 148
421 490 tracker_id: 2
422 workflows_174:
491 type: WorkflowTransition
492 WorkflowTransitions_174:
423 493 new_status_id: 4
424 494 role_id: 3
425 495 old_status_id: 5
426 496 id: 174
427 497 tracker_id: 2
428 workflows_256:
498 type: WorkflowTransition
499 WorkflowTransitions_256:
429 500 new_status_id: 1
430 501 role_id: 3
431 502 old_status_id: 4
432 503 id: 256
433 504 tracker_id: 3
434 workflows_068:
505 type: WorkflowTransition
506 WorkflowTransitions_068:
435 507 new_status_id: 4
436 508 role_id: 3
437 509 old_status_id: 2
438 510 id: 68
439 511 tracker_id: 1
440 workflows_094:
512 type: WorkflowTransition
513 WorkflowTransitions_094:
441 514 new_status_id: 5
442 515 role_id: 1
443 516 old_status_id: 1
444 517 id: 94
445 518 tracker_id: 2
446 workflows_149:
519 type: WorkflowTransition
520 WorkflowTransitions_149:
447 521 new_status_id: 4
448 522 role_id: 2
449 523 old_status_id: 6
450 524 id: 149
451 525 tracker_id: 2
452 workflows_175:
526 type: WorkflowTransition
527 WorkflowTransitions_175:
453 528 new_status_id: 6
454 529 role_id: 3
455 530 old_status_id: 5
456 531 id: 175
457 532 tracker_id: 2
458 workflows_257:
533 type: WorkflowTransition
534 WorkflowTransitions_257:
459 535 new_status_id: 2
460 536 role_id: 3
461 537 old_status_id: 4
462 538 id: 257
463 539 tracker_id: 3
464 workflows_069:
540 type: WorkflowTransition
541 WorkflowTransitions_069:
465 542 new_status_id: 5
466 543 role_id: 3
467 544 old_status_id: 2
468 545 id: 69
469 546 tracker_id: 1
470 workflows_095:
547 type: WorkflowTransition
548 WorkflowTransitions_095:
471 549 new_status_id: 6
472 550 role_id: 1
473 551 old_status_id: 1
474 552 id: 95
475 553 tracker_id: 2
476 workflows_176:
554 type: WorkflowTransition
555 WorkflowTransitions_176:
477 556 new_status_id: 1
478 557 role_id: 3
479 558 old_status_id: 6
480 559 id: 176
481 560 tracker_id: 2
482 workflows_258:
561 type: WorkflowTransition
562 WorkflowTransitions_258:
483 563 new_status_id: 3
484 564 role_id: 3
485 565 old_status_id: 4
486 566 id: 258
487 567 tracker_id: 3
488 workflows_096:
568 type: WorkflowTransition
569 WorkflowTransitions_096:
489 570 new_status_id: 1
490 571 role_id: 1
491 572 old_status_id: 2
492 573 id: 96
493 574 tracker_id: 2
494 workflows_177:
575 type: WorkflowTransition
576 WorkflowTransitions_177:
495 577 new_status_id: 2
496 578 role_id: 3
497 579 old_status_id: 6
498 580 id: 177
499 581 tracker_id: 2
500 workflows_259:
582 type: WorkflowTransition
583 WorkflowTransitions_259:
501 584 new_status_id: 5
502 585 role_id: 3
503 586 old_status_id: 4
504 587 id: 259
505 588 tracker_id: 3
506 workflows_097:
589 type: WorkflowTransition
590 WorkflowTransitions_097:
507 591 new_status_id: 3
508 592 role_id: 1
509 593 old_status_id: 2
510 594 id: 97
511 595 tracker_id: 2
512 workflows_178:
596 type: WorkflowTransition
597 WorkflowTransitions_178:
513 598 new_status_id: 3
514 599 role_id: 3
515 600 old_status_id: 6
516 601 id: 178
517 602 tracker_id: 2
518 workflows_098:
603 type: WorkflowTransition
604 WorkflowTransitions_098:
519 605 new_status_id: 4
520 606 role_id: 1
521 607 old_status_id: 2
522 608 id: 98
523 609 tracker_id: 2
524 workflows_179:
610 type: WorkflowTransition
611 WorkflowTransitions_179:
525 612 new_status_id: 4
526 613 role_id: 3
527 614 old_status_id: 6
528 615 id: 179
529 616 tracker_id: 2
530 workflows_099:
617 type: WorkflowTransition
618 WorkflowTransitions_099:
531 619 new_status_id: 5
532 620 role_id: 1
533 621 old_status_id: 2
534 622 id: 99
535 623 tracker_id: 2
536 workflows_100:
624 type: WorkflowTransition
625 WorkflowTransitions_100:
537 626 new_status_id: 6
538 627 role_id: 1
539 628 old_status_id: 2
540 629 id: 100
541 630 tracker_id: 2
542 workflows_020:
631 type: WorkflowTransition
632 WorkflowTransitions_020:
543 633 new_status_id: 6
544 634 role_id: 1
545 635 old_status_id: 4
546 636 id: 20
547 637 tracker_id: 1
548 workflows_101:
638 type: WorkflowTransition
639 WorkflowTransitions_101:
549 640 new_status_id: 1
550 641 role_id: 1
551 642 old_status_id: 3
552 643 id: 101
553 644 tracker_id: 2
554 workflows_021:
645 type: WorkflowTransition
646 WorkflowTransitions_021:
555 647 new_status_id: 1
556 648 role_id: 1
557 649 old_status_id: 5
558 650 id: 21
559 651 tracker_id: 1
560 workflows_102:
652 type: WorkflowTransition
653 WorkflowTransitions_102:
561 654 new_status_id: 2
562 655 role_id: 1
563 656 old_status_id: 3
564 657 id: 102
565 658 tracker_id: 2
566 workflows_210:
659 type: WorkflowTransition
660 WorkflowTransitions_210:
567 661 new_status_id: 5
568 662 role_id: 1
569 663 old_status_id: 6
570 664 id: 210
571 665 tracker_id: 3
572 workflows_022:
666 type: WorkflowTransition
667 WorkflowTransitions_022:
573 668 new_status_id: 2
574 669 role_id: 1
575 670 old_status_id: 5
576 671 id: 22
577 672 tracker_id: 1
578 workflows_103:
673 type: WorkflowTransition
674 WorkflowTransitions_103:
579 675 new_status_id: 4
580 676 role_id: 1
581 677 old_status_id: 3
582 678 id: 103
583 679 tracker_id: 2
584 workflows_023:
680 type: WorkflowTransition
681 WorkflowTransitions_023:
585 682 new_status_id: 3
586 683 role_id: 1
587 684 old_status_id: 5
588 685 id: 23
589 686 tracker_id: 1
590 workflows_104:
687 type: WorkflowTransition
688 WorkflowTransitions_104:
591 689 new_status_id: 5
592 690 role_id: 1
593 691 old_status_id: 3
594 692 id: 104
595 693 tracker_id: 2
596 workflows_130:
694 type: WorkflowTransition
695 WorkflowTransitions_130:
597 696 new_status_id: 6
598 697 role_id: 2
599 698 old_status_id: 2
600 699 id: 130
601 700 tracker_id: 2
602 workflows_211:
701 type: WorkflowTransition
702 WorkflowTransitions_211:
603 703 new_status_id: 2
604 704 role_id: 2
605 705 old_status_id: 1
606 706 id: 211
607 707 tracker_id: 3
608 workflows_024:
708 type: WorkflowTransition
709 WorkflowTransitions_024:
609 710 new_status_id: 4
610 711 role_id: 1
611 712 old_status_id: 5
612 713 id: 24
613 714 tracker_id: 1
614 workflows_050:
715 type: WorkflowTransition
716 WorkflowTransitions_050:
615 717 new_status_id: 6
616 718 role_id: 2
617 719 old_status_id: 4
618 720 id: 50
619 721 tracker_id: 1
620 workflows_105:
722 type: WorkflowTransition
723 WorkflowTransitions_105:
621 724 new_status_id: 6
622 725 role_id: 1
623 726 old_status_id: 3
624 727 id: 105
625 728 tracker_id: 2
626 workflows_131:
729 type: WorkflowTransition
730 WorkflowTransitions_131:
627 731 new_status_id: 1
628 732 role_id: 2
629 733 old_status_id: 3
630 734 id: 131
631 735 tracker_id: 2
632 workflows_212:
736 type: WorkflowTransition
737 WorkflowTransitions_212:
633 738 new_status_id: 3
634 739 role_id: 2
635 740 old_status_id: 1
636 741 id: 212
637 742 tracker_id: 3
638 workflows_025:
743 type: WorkflowTransition
744 WorkflowTransitions_025:
639 745 new_status_id: 6
640 746 role_id: 1
641 747 old_status_id: 5
642 748 id: 25
643 749 tracker_id: 1
644 workflows_051:
750 type: WorkflowTransition
751 WorkflowTransitions_051:
645 752 new_status_id: 1
646 753 role_id: 2
647 754 old_status_id: 5
648 755 id: 51
649 756 tracker_id: 1
650 workflows_106:
757 type: WorkflowTransition
758 WorkflowTransitions_106:
651 759 new_status_id: 1
652 760 role_id: 1
653 761 old_status_id: 4
654 762 id: 106
655 763 tracker_id: 2
656 workflows_132:
764 type: WorkflowTransition
765 WorkflowTransitions_132:
657 766 new_status_id: 2
658 767 role_id: 2
659 768 old_status_id: 3
660 769 id: 132
661 770 tracker_id: 2
662 workflows_213:
771 type: WorkflowTransition
772 WorkflowTransitions_213:
663 773 new_status_id: 4
664 774 role_id: 2
665 775 old_status_id: 1
666 776 id: 213
667 777 tracker_id: 3
668 workflows_240:
778 type: WorkflowTransition
779 WorkflowTransitions_240:
669 780 new_status_id: 5
670 781 role_id: 2
671 782 old_status_id: 6
672 783 id: 240
673 784 tracker_id: 3
674 workflows_026:
785 type: WorkflowTransition
786 WorkflowTransitions_026:
675 787 new_status_id: 1
676 788 role_id: 1
677 789 old_status_id: 6
678 790 id: 26
679 791 tracker_id: 1
680 workflows_052:
792 type: WorkflowTransition
793 WorkflowTransitions_052:
681 794 new_status_id: 2
682 795 role_id: 2
683 796 old_status_id: 5
684 797 id: 52
685 798 tracker_id: 1
686 workflows_107:
799 type: WorkflowTransition
800 WorkflowTransitions_107:
687 801 new_status_id: 2
688 802 role_id: 1
689 803 old_status_id: 4
690 804 id: 107
691 805 tracker_id: 2
692 workflows_133:
806 type: WorkflowTransition
807 WorkflowTransitions_133:
693 808 new_status_id: 4
694 809 role_id: 2
695 810 old_status_id: 3
696 811 id: 133
697 812 tracker_id: 2
698 workflows_214:
813 type: WorkflowTransition
814 WorkflowTransitions_214:
699 815 new_status_id: 5
700 816 role_id: 2
701 817 old_status_id: 1
702 818 id: 214
703 819 tracker_id: 3
704 workflows_241:
820 type: WorkflowTransition
821 WorkflowTransitions_241:
705 822 new_status_id: 2
706 823 role_id: 3
707 824 old_status_id: 1
708 825 id: 241
709 826 tracker_id: 3
710 workflows_027:
827 type: WorkflowTransition
828 WorkflowTransitions_027:
711 829 new_status_id: 2
712 830 role_id: 1
713 831 old_status_id: 6
714 832 id: 27
715 833 tracker_id: 1
716 workflows_053:
834 type: WorkflowTransition
835 WorkflowTransitions_053:
717 836 new_status_id: 3
718 837 role_id: 2
719 838 old_status_id: 5
720 839 id: 53
721 840 tracker_id: 1
722 workflows_080:
841 type: WorkflowTransition
842 WorkflowTransitions_080:
723 843 new_status_id: 6
724 844 role_id: 3
725 845 old_status_id: 4
726 846 id: 80
727 847 tracker_id: 1
728 workflows_108:
848 type: WorkflowTransition
849 WorkflowTransitions_108:
729 850 new_status_id: 3
730 851 role_id: 1
731 852 old_status_id: 4
732 853 id: 108
733 854 tracker_id: 2
734 workflows_134:
855 type: WorkflowTransition
856 WorkflowTransitions_134:
735 857 new_status_id: 5
736 858 role_id: 2
737 859 old_status_id: 3
738 860 id: 134
739 861 tracker_id: 2
740 workflows_160:
862 type: WorkflowTransition
863 WorkflowTransitions_160:
741 864 new_status_id: 6
742 865 role_id: 3
743 866 old_status_id: 2
744 867 id: 160
745 868 tracker_id: 2
746 workflows_215:
869 type: WorkflowTransition
870 WorkflowTransitions_215:
747 871 new_status_id: 6
748 872 role_id: 2
749 873 old_status_id: 1
750 874 id: 215
751 875 tracker_id: 3
752 workflows_242:
876 type: WorkflowTransition
877 WorkflowTransitions_242:
753 878 new_status_id: 3
754 879 role_id: 3
755 880 old_status_id: 1
756 881 id: 242
757 882 tracker_id: 3
758 workflows_028:
883 type: WorkflowTransition
884 WorkflowTransitions_028:
759 885 new_status_id: 3
760 886 role_id: 1
761 887 old_status_id: 6
762 888 id: 28
763 889 tracker_id: 1
764 workflows_054:
890 type: WorkflowTransition
891 WorkflowTransitions_054:
765 892 new_status_id: 4
766 893 role_id: 2
767 894 old_status_id: 5
768 895 id: 54
769 896 tracker_id: 1
770 workflows_081:
897 type: WorkflowTransition
898 WorkflowTransitions_081:
771 899 new_status_id: 1
772 900 role_id: 3
773 901 old_status_id: 5
774 902 id: 81
775 903 tracker_id: 1
776 workflows_109:
904 type: WorkflowTransition
905 WorkflowTransitions_109:
777 906 new_status_id: 5
778 907 role_id: 1
779 908 old_status_id: 4
780 909 id: 109
781 910 tracker_id: 2
782 workflows_135:
911 type: WorkflowTransition
912 WorkflowTransitions_135:
783 913 new_status_id: 6
784 914 role_id: 2
785 915 old_status_id: 3
786 916 id: 135
787 917 tracker_id: 2
788 workflows_161:
918 type: WorkflowTransition
919 WorkflowTransitions_161:
789 920 new_status_id: 1
790 921 role_id: 3
791 922 old_status_id: 3
792 923 id: 161
793 924 tracker_id: 2
794 workflows_216:
925 type: WorkflowTransition
926 WorkflowTransitions_216:
795 927 new_status_id: 1
796 928 role_id: 2
797 929 old_status_id: 2
798 930 id: 216
799 931 tracker_id: 3
800 workflows_243:
932 type: WorkflowTransition
933 WorkflowTransitions_243:
801 934 new_status_id: 4
802 935 role_id: 3
803 936 old_status_id: 1
804 937 id: 243
805 938 tracker_id: 3
806 workflows_029:
939 type: WorkflowTransition
940 WorkflowTransitions_029:
807 941 new_status_id: 4
808 942 role_id: 1
809 943 old_status_id: 6
810 944 id: 29
811 945 tracker_id: 1
812 workflows_055:
946 type: WorkflowTransition
947 WorkflowTransitions_055:
813 948 new_status_id: 6
814 949 role_id: 2
815 950 old_status_id: 5
816 951 id: 55
817 952 tracker_id: 1
818 workflows_082:
953 type: WorkflowTransition
954 WorkflowTransitions_082:
819 955 new_status_id: 2
820 956 role_id: 3
821 957 old_status_id: 5
822 958 id: 82
823 959 tracker_id: 1
824 workflows_136:
960 type: WorkflowTransition
961 WorkflowTransitions_136:
825 962 new_status_id: 1
826 963 role_id: 2
827 964 old_status_id: 4
828 965 id: 136
829 966 tracker_id: 2
830 workflows_162:
967 type: WorkflowTransition
968 WorkflowTransitions_162:
831 969 new_status_id: 2
832 970 role_id: 3
833 971 old_status_id: 3
834 972 id: 162
835 973 tracker_id: 2
836 workflows_217:
974 type: WorkflowTransition
975 WorkflowTransitions_217:
837 976 new_status_id: 3
838 977 role_id: 2
839 978 old_status_id: 2
840 979 id: 217
841 980 tracker_id: 3
842 workflows_270:
981 type: WorkflowTransition
982 WorkflowTransitions_270:
843 983 new_status_id: 5
844 984 role_id: 3
845 985 old_status_id: 6
846 986 id: 270
847 987 tracker_id: 3
848 workflows_244:
988 type: WorkflowTransition
989 WorkflowTransitions_244:
849 990 new_status_id: 5
850 991 role_id: 3
851 992 old_status_id: 1
852 993 id: 244
853 994 tracker_id: 3
854 workflows_056:
995 type: WorkflowTransition
996 WorkflowTransitions_056:
855 997 new_status_id: 1
856 998 role_id: 2
857 999 old_status_id: 6
858 1000 id: 56
859 1001 tracker_id: 1
860 workflows_137:
1002 type: WorkflowTransition
1003 WorkflowTransitions_137:
861 1004 new_status_id: 2
862 1005 role_id: 2
863 1006 old_status_id: 4
864 1007 id: 137
865 1008 tracker_id: 2
866 workflows_163:
1009 type: WorkflowTransition
1010 WorkflowTransitions_163:
867 1011 new_status_id: 4
868 1012 role_id: 3
869 1013 old_status_id: 3
870 1014 id: 163
871 1015 tracker_id: 2
872 workflows_190:
1016 type: WorkflowTransition
1017 WorkflowTransitions_190:
873 1018 new_status_id: 6
874 1019 role_id: 1
875 1020 old_status_id: 2
876 1021 id: 190
877 1022 tracker_id: 3
878 workflows_218:
1023 type: WorkflowTransition
1024 WorkflowTransitions_218:
879 1025 new_status_id: 4
880 1026 role_id: 2
881 1027 old_status_id: 2
882 1028 id: 218
883 1029 tracker_id: 3
884 workflows_245:
1030 type: WorkflowTransition
1031 WorkflowTransitions_245:
885 1032 new_status_id: 6
886 1033 role_id: 3
887 1034 old_status_id: 1
888 1035 id: 245
889 1036 tracker_id: 3
890 workflows_057:
1037 type: WorkflowTransition
1038 WorkflowTransitions_057:
891 1039 new_status_id: 2
892 1040 role_id: 2
893 1041 old_status_id: 6
894 1042 id: 57
895 1043 tracker_id: 1
896 workflows_083:
1044 type: WorkflowTransition
1045 WorkflowTransitions_083:
897 1046 new_status_id: 3
898 1047 role_id: 3
899 1048 old_status_id: 5
900 1049 id: 83
901 1050 tracker_id: 1
902 workflows_138:
1051 type: WorkflowTransition
1052 WorkflowTransitions_138:
903 1053 new_status_id: 3
904 1054 role_id: 2
905 1055 old_status_id: 4
906 1056 id: 138
907 1057 tracker_id: 2
908 workflows_164:
1058 type: WorkflowTransition
1059 WorkflowTransitions_164:
909 1060 new_status_id: 5
910 1061 role_id: 3
911 1062 old_status_id: 3
912 1063 id: 164
913 1064 tracker_id: 2
914 workflows_191:
1065 type: WorkflowTransition
1066 WorkflowTransitions_191:
915 1067 new_status_id: 1
916 1068 role_id: 1
917 1069 old_status_id: 3
918 1070 id: 191
919 1071 tracker_id: 3
920 workflows_219:
1072 type: WorkflowTransition
1073 WorkflowTransitions_219:
921 1074 new_status_id: 5
922 1075 role_id: 2
923 1076 old_status_id: 2
924 1077 id: 219
925 1078 tracker_id: 3
926 workflows_246:
1079 type: WorkflowTransition
1080 WorkflowTransitions_246:
927 1081 new_status_id: 1
928 1082 role_id: 3
929 1083 old_status_id: 2
930 1084 id: 246
931 1085 tracker_id: 3
932 workflows_058:
1086 type: WorkflowTransition
1087 WorkflowTransitions_058:
933 1088 new_status_id: 3
934 1089 role_id: 2
935 1090 old_status_id: 6
936 1091 id: 58
937 1092 tracker_id: 1
938 workflows_084:
1093 type: WorkflowTransition
1094 WorkflowTransitions_084:
939 1095 new_status_id: 4
940 1096 role_id: 3
941 1097 old_status_id: 5
942 1098 id: 84
943 1099 tracker_id: 1
944 workflows_139:
1100 type: WorkflowTransition
1101 WorkflowTransitions_139:
945 1102 new_status_id: 5
946 1103 role_id: 2
947 1104 old_status_id: 4
948 1105 id: 139
949 1106 tracker_id: 2
950 workflows_165:
1107 type: WorkflowTransition
1108 WorkflowTransitions_165:
951 1109 new_status_id: 6
952 1110 role_id: 3
953 1111 old_status_id: 3
954 1112 id: 165
955 1113 tracker_id: 2
956 workflows_192:
1114 type: WorkflowTransition
1115 WorkflowTransitions_192:
957 1116 new_status_id: 2
958 1117 role_id: 1
959 1118 old_status_id: 3
960 1119 id: 192
961 1120 tracker_id: 3
962 workflows_247:
1121 type: WorkflowTransition
1122 WorkflowTransitions_247:
963 1123 new_status_id: 3
964 1124 role_id: 3
965 1125 old_status_id: 2
966 1126 id: 247
967 1127 tracker_id: 3
968 workflows_059:
1128 type: WorkflowTransition
1129 WorkflowTransitions_059:
969 1130 new_status_id: 4
970 1131 role_id: 2
971 1132 old_status_id: 6
972 1133 id: 59
973 1134 tracker_id: 1
974 workflows_085:
1135 type: WorkflowTransition
1136 WorkflowTransitions_085:
975 1137 new_status_id: 6
976 1138 role_id: 3
977 1139 old_status_id: 5
978 1140 id: 85
979 1141 tracker_id: 1
980 workflows_166:
1142 type: WorkflowTransition
1143 WorkflowTransitions_166:
981 1144 new_status_id: 1
982 1145 role_id: 3
983 1146 old_status_id: 4
984 1147 id: 166
985 1148 tracker_id: 2
986 workflows_248:
1149 type: WorkflowTransition
1150 WorkflowTransitions_248:
987 1151 new_status_id: 4
988 1152 role_id: 3
989 1153 old_status_id: 2
990 1154 id: 248
991 1155 tracker_id: 3
992 workflows_086:
1156 type: WorkflowTransition
1157 WorkflowTransitions_086:
993 1158 new_status_id: 1
994 1159 role_id: 3
995 1160 old_status_id: 6
996 1161 id: 86
997 1162 tracker_id: 1
998 workflows_167:
1163 type: WorkflowTransition
1164 WorkflowTransitions_167:
999 1165 new_status_id: 2
1000 1166 role_id: 3
1001 1167 old_status_id: 4
1002 1168 id: 167
1003 1169 tracker_id: 2
1004 workflows_193:
1170 type: WorkflowTransition
1171 WorkflowTransitions_193:
1005 1172 new_status_id: 4
1006 1173 role_id: 1
1007 1174 old_status_id: 3
1008 1175 id: 193
1009 1176 tracker_id: 3
1010 workflows_249:
1177 type: WorkflowTransition
1178 WorkflowTransitions_249:
1011 1179 new_status_id: 5
1012 1180 role_id: 3
1013 1181 old_status_id: 2
1014 1182 id: 249
1015 1183 tracker_id: 3
1016 workflows_087:
1184 type: WorkflowTransition
1185 WorkflowTransitions_087:
1017 1186 new_status_id: 2
1018 1187 role_id: 3
1019 1188 old_status_id: 6
1020 1189 id: 87
1021 1190 tracker_id: 1
1022 workflows_168:
1191 type: WorkflowTransition
1192 WorkflowTransitions_168:
1023 1193 new_status_id: 3
1024 1194 role_id: 3
1025 1195 old_status_id: 4
1026 1196 id: 168
1027 1197 tracker_id: 2
1028 workflows_194:
1198 type: WorkflowTransition
1199 WorkflowTransitions_194:
1029 1200 new_status_id: 5
1030 1201 role_id: 1
1031 1202 old_status_id: 3
1032 1203 id: 194
1033 1204 tracker_id: 3
1034 workflows_088:
1205 type: WorkflowTransition
1206 WorkflowTransitions_088:
1035 1207 new_status_id: 3
1036 1208 role_id: 3
1037 1209 old_status_id: 6
1038 1210 id: 88
1039 1211 tracker_id: 1
1040 workflows_169:
1212 type: WorkflowTransition
1213 WorkflowTransitions_169:
1041 1214 new_status_id: 5
1042 1215 role_id: 3
1043 1216 old_status_id: 4
1044 1217 id: 169
1045 1218 tracker_id: 2
1046 workflows_195:
1219 type: WorkflowTransition
1220 WorkflowTransitions_195:
1047 1221 new_status_id: 6
1048 1222 role_id: 1
1049 1223 old_status_id: 3
1050 1224 id: 195
1051 1225 tracker_id: 3
1052 workflows_089:
1226 type: WorkflowTransition
1227 WorkflowTransitions_089:
1053 1228 new_status_id: 4
1054 1229 role_id: 3
1055 1230 old_status_id: 6
1056 1231 id: 89
1057 1232 tracker_id: 1
1058 workflows_196:
1233 type: WorkflowTransition
1234 WorkflowTransitions_196:
1059 1235 new_status_id: 1
1060 1236 role_id: 1
1061 1237 old_status_id: 4
1062 1238 id: 196
1063 1239 tracker_id: 3
1064 workflows_197:
1240 type: WorkflowTransition
1241 WorkflowTransitions_197:
1065 1242 new_status_id: 2
1066 1243 role_id: 1
1067 1244 old_status_id: 4
1068 1245 id: 197
1069 1246 tracker_id: 3
1070 workflows_198:
1247 type: WorkflowTransition
1248 WorkflowTransitions_198:
1071 1249 new_status_id: 3
1072 1250 role_id: 1
1073 1251 old_status_id: 4
1074 1252 id: 198
1075 1253 tracker_id: 3
1076 workflows_199:
1254 type: WorkflowTransition
1255 WorkflowTransitions_199:
1077 1256 new_status_id: 5
1078 1257 role_id: 1
1079 1258 old_status_id: 4
1080 1259 id: 199
1081 1260 tracker_id: 3
1082 workflows_010:
1261 type: WorkflowTransition
1262 WorkflowTransitions_010:
1083 1263 new_status_id: 6
1084 1264 role_id: 1
1085 1265 old_status_id: 2
1086 1266 id: 10
1087 1267 tracker_id: 1
1088 workflows_011:
1268 type: WorkflowTransition
1269 WorkflowTransitions_011:
1089 1270 new_status_id: 1
1090 1271 role_id: 1
1091 1272 old_status_id: 3
1092 1273 id: 11
1093 1274 tracker_id: 1
1094 workflows_012:
1275 type: WorkflowTransition
1276 WorkflowTransitions_012:
1095 1277 new_status_id: 2
1096 1278 role_id: 1
1097 1279 old_status_id: 3
1098 1280 id: 12
1099 1281 tracker_id: 1
1100 workflows_200:
1282 type: WorkflowTransition
1283 WorkflowTransitions_200:
1101 1284 new_status_id: 6
1102 1285 role_id: 1
1103 1286 old_status_id: 4
1104 1287 id: 200
1105 1288 tracker_id: 3
1106 workflows_013:
1289 type: WorkflowTransition
1290 WorkflowTransitions_013:
1107 1291 new_status_id: 4
1108 1292 role_id: 1
1109 1293 old_status_id: 3
1110 1294 id: 13
1111 1295 tracker_id: 1
1112 workflows_120:
1296 type: WorkflowTransition
1297 WorkflowTransitions_120:
1113 1298 new_status_id: 5
1114 1299 role_id: 1
1115 1300 old_status_id: 6
1116 1301 id: 120
1117 1302 tracker_id: 2
1118 workflows_201:
1303 type: WorkflowTransition
1304 WorkflowTransitions_201:
1119 1305 new_status_id: 1
1120 1306 role_id: 1
1121 1307 old_status_id: 5
1122 1308 id: 201
1123 1309 tracker_id: 3
1124 workflows_040:
1310 type: WorkflowTransition
1311 WorkflowTransitions_040:
1125 1312 new_status_id: 6
1126 1313 role_id: 2
1127 1314 old_status_id: 2
1128 1315 id: 40
1129 1316 tracker_id: 1
1130 workflows_121:
1317 type: WorkflowTransition
1318 WorkflowTransitions_121:
1131 1319 new_status_id: 2
1132 1320 role_id: 2
1133 1321 old_status_id: 1
1134 1322 id: 121
1135 1323 tracker_id: 2
1136 workflows_202:
1324 type: WorkflowTransition
1325 WorkflowTransitions_202:
1137 1326 new_status_id: 2
1138 1327 role_id: 1
1139 1328 old_status_id: 5
1140 1329 id: 202
1141 1330 tracker_id: 3
1142 workflows_014:
1331 type: WorkflowTransition
1332 WorkflowTransitions_014:
1143 1333 new_status_id: 5
1144 1334 role_id: 1
1145 1335 old_status_id: 3
1146 1336 id: 14
1147 1337 tracker_id: 1
1148 workflows_041:
1338 type: WorkflowTransition
1339 WorkflowTransitions_041:
1149 1340 new_status_id: 1
1150 1341 role_id: 2
1151 1342 old_status_id: 3
1152 1343 id: 41
1153 1344 tracker_id: 1
1154 workflows_122:
1345 type: WorkflowTransition
1346 WorkflowTransitions_122:
1155 1347 new_status_id: 3
1156 1348 role_id: 2
1157 1349 old_status_id: 1
1158 1350 id: 122
1159 1351 tracker_id: 2
1160 workflows_203:
1352 type: WorkflowTransition
1353 WorkflowTransitions_203:
1161 1354 new_status_id: 3
1162 1355 role_id: 1
1163 1356 old_status_id: 5
1164 1357 id: 203
1165 1358 tracker_id: 3
1166 workflows_015:
1359 type: WorkflowTransition
1360 WorkflowTransitions_015:
1167 1361 new_status_id: 6
1168 1362 role_id: 1
1169 1363 old_status_id: 3
1170 1364 id: 15
1171 1365 tracker_id: 1
1172 workflows_230:
1366 type: WorkflowTransition
1367 WorkflowTransitions_230:
1173 1368 new_status_id: 6
1174 1369 role_id: 2
1175 1370 old_status_id: 4
1176 1371 id: 230
1177 1372 tracker_id: 3
1178 workflows_123:
1373 type: WorkflowTransition
1374 WorkflowTransitions_123:
1179 1375 new_status_id: 4
1180 1376 role_id: 2
1181 1377 old_status_id: 1
1182 1378 id: 123
1183 1379 tracker_id: 2
1184 workflows_204:
1380 type: WorkflowTransition
1381 WorkflowTransitions_204:
1185 1382 new_status_id: 4
1186 1383 role_id: 1
1187 1384 old_status_id: 5
1188 1385 id: 204
1189 1386 tracker_id: 3
1190 workflows_016:
1387 type: WorkflowTransition
1388 WorkflowTransitions_016:
1191 1389 new_status_id: 1
1192 1390 role_id: 1
1193 1391 old_status_id: 4
1194 1392 id: 16
1195 1393 tracker_id: 1
1196 workflows_042:
1394 type: WorkflowTransition
1395 WorkflowTransitions_042:
1197 1396 new_status_id: 2
1198 1397 role_id: 2
1199 1398 old_status_id: 3
1200 1399 id: 42
1201 1400 tracker_id: 1
1202 workflows_231:
1401 type: WorkflowTransition
1402 WorkflowTransitions_231:
1203 1403 new_status_id: 1
1204 1404 role_id: 2
1205 1405 old_status_id: 5
1206 1406 id: 231
1207 1407 tracker_id: 3
1208 workflows_070:
1408 type: WorkflowTransition
1409 WorkflowTransitions_070:
1209 1410 new_status_id: 6
1210 1411 role_id: 3
1211 1412 old_status_id: 2
1212 1413 id: 70
1213 1414 tracker_id: 1
1214 workflows_124:
1415 type: WorkflowTransition
1416 WorkflowTransitions_124:
1215 1417 new_status_id: 5
1216 1418 role_id: 2
1217 1419 old_status_id: 1
1218 1420 id: 124
1219 1421 tracker_id: 2
1220 workflows_150:
1422 type: WorkflowTransition
1423 WorkflowTransitions_150:
1221 1424 new_status_id: 5
1222 1425 role_id: 2
1223 1426 old_status_id: 6
1224 1427 id: 150
1225 1428 tracker_id: 2
1226 workflows_205:
1429 type: WorkflowTransition
1430 WorkflowTransitions_205:
1227 1431 new_status_id: 6
1228 1432 role_id: 1
1229 1433 old_status_id: 5
1230 1434 id: 205
1231 1435 tracker_id: 3
1232 workflows_017:
1436 type: WorkflowTransition
1437 WorkflowTransitions_017:
1233 1438 new_status_id: 2
1234 1439 role_id: 1
1235 1440 old_status_id: 4
1236 1441 id: 17
1237 1442 tracker_id: 1
1238 workflows_043:
1443 type: WorkflowTransition
1444 WorkflowTransitions_043:
1239 1445 new_status_id: 4
1240 1446 role_id: 2
1241 1447 old_status_id: 3
1242 1448 id: 43
1243 1449 tracker_id: 1
1244 workflows_232:
1450 type: WorkflowTransition
1451 WorkflowTransitions_232:
1245 1452 new_status_id: 2
1246 1453 role_id: 2
1247 1454 old_status_id: 5
1248 1455 id: 232
1249 1456 tracker_id: 3
1250 workflows_125:
1457 type: WorkflowTransition
1458 WorkflowTransitions_125:
1251 1459 new_status_id: 6
1252 1460 role_id: 2
1253 1461 old_status_id: 1
1254 1462 id: 125
1255 1463 tracker_id: 2
1256 workflows_151:
1464 type: WorkflowTransition
1465 WorkflowTransitions_151:
1257 1466 new_status_id: 2
1258 1467 role_id: 3
1259 1468 old_status_id: 1
1260 1469 id: 151
1261 1470 tracker_id: 2
1262 workflows_206:
1471 type: WorkflowTransition
1472 WorkflowTransitions_206:
1263 1473 new_status_id: 1
1264 1474 role_id: 1
1265 1475 old_status_id: 6
1266 1476 id: 206
1267 1477 tracker_id: 3
1268 workflows_018:
1478 type: WorkflowTransition
1479 WorkflowTransitions_018:
1269 1480 new_status_id: 3
1270 1481 role_id: 1
1271 1482 old_status_id: 4
1272 1483 id: 18
1273 1484 tracker_id: 1
1274 workflows_044:
1485 type: WorkflowTransition
1486 WorkflowTransitions_044:
1275 1487 new_status_id: 5
1276 1488 role_id: 2
1277 1489 old_status_id: 3
1278 1490 id: 44
1279 1491 tracker_id: 1
1280 workflows_071:
1492 type: WorkflowTransition
1493 WorkflowTransitions_071:
1281 1494 new_status_id: 1
1282 1495 role_id: 3
1283 1496 old_status_id: 3
1284 1497 id: 71
1285 1498 tracker_id: 1
1286 workflows_233:
1499 type: WorkflowTransition
1500 WorkflowTransitions_233:
1287 1501 new_status_id: 3
1288 1502 role_id: 2
1289 1503 old_status_id: 5
1290 1504 id: 233
1291 1505 tracker_id: 3
1292 workflows_126:
1506 type: WorkflowTransition
1507 WorkflowTransitions_126:
1293 1508 new_status_id: 1
1294 1509 role_id: 2
1295 1510 old_status_id: 2
1296 1511 id: 126
1297 1512 tracker_id: 2
1298 workflows_152:
1513 type: WorkflowTransition
1514 WorkflowTransitions_152:
1299 1515 new_status_id: 3
1300 1516 role_id: 3
1301 1517 old_status_id: 1
1302 1518 id: 152
1303 1519 tracker_id: 2
1304 workflows_207:
1520 type: WorkflowTransition
1521 WorkflowTransitions_207:
1305 1522 new_status_id: 2
1306 1523 role_id: 1
1307 1524 old_status_id: 6
1308 1525 id: 207
1309 1526 tracker_id: 3
1310 workflows_019:
1527 type: WorkflowTransition
1528 WorkflowTransitions_019:
1311 1529 new_status_id: 5
1312 1530 role_id: 1
1313 1531 old_status_id: 4
1314 1532 id: 19
1315 1533 tracker_id: 1
1316 workflows_045:
1534 type: WorkflowTransition
1535 WorkflowTransitions_045:
1317 1536 new_status_id: 6
1318 1537 role_id: 2
1319 1538 old_status_id: 3
1320 1539 id: 45
1321 1540 tracker_id: 1
1322 workflows_260:
1541 type: WorkflowTransition
1542 WorkflowTransitions_260:
1323 1543 new_status_id: 6
1324 1544 role_id: 3
1325 1545 old_status_id: 4
1326 1546 id: 260
1327 1547 tracker_id: 3
1328 workflows_234:
1548 type: WorkflowTransition
1549 WorkflowTransitions_234:
1329 1550 new_status_id: 4
1330 1551 role_id: 2
1331 1552 old_status_id: 5
1332 1553 id: 234
1333 1554 tracker_id: 3
1334 workflows_127:
1555 type: WorkflowTransition
1556 WorkflowTransitions_127:
1335 1557 new_status_id: 3
1336 1558 role_id: 2
1337 1559 old_status_id: 2
1338 1560 id: 127
1339 1561 tracker_id: 2
1340 workflows_153:
1562 type: WorkflowTransition
1563 WorkflowTransitions_153:
1341 1564 new_status_id: 4
1342 1565 role_id: 3
1343 1566 old_status_id: 1
1344 1567 id: 153
1345 1568 tracker_id: 2
1346 workflows_180:
1569 type: WorkflowTransition
1570 WorkflowTransitions_180:
1347 1571 new_status_id: 5
1348 1572 role_id: 3
1349 1573 old_status_id: 6
1350 1574 id: 180
1351 1575 tracker_id: 2
1352 workflows_208:
1576 type: WorkflowTransition
1577 WorkflowTransitions_208:
1353 1578 new_status_id: 3
1354 1579 role_id: 1
1355 1580 old_status_id: 6
1356 1581 id: 208
1357 1582 tracker_id: 3
1358 workflows_046:
1583 type: WorkflowTransition
1584 WorkflowTransitions_046:
1359 1585 new_status_id: 1
1360 1586 role_id: 2
1361 1587 old_status_id: 4
1362 1588 id: 46
1363 1589 tracker_id: 1
1364 workflows_072:
1590 type: WorkflowTransition
1591 WorkflowTransitions_072:
1365 1592 new_status_id: 2
1366 1593 role_id: 3
1367 1594 old_status_id: 3
1368 1595 id: 72
1369 1596 tracker_id: 1
1370 workflows_261:
1597 type: WorkflowTransition
1598 WorkflowTransitions_261:
1371 1599 new_status_id: 1
1372 1600 role_id: 3
1373 1601 old_status_id: 5
1374 1602 id: 261
1375 1603 tracker_id: 3
1376 workflows_235:
1604 type: WorkflowTransition
1605 WorkflowTransitions_235:
1377 1606 new_status_id: 6
1378 1607 role_id: 2
1379 1608 old_status_id: 5
1380 1609 id: 235
1381 1610 tracker_id: 3
1382 workflows_154:
1611 type: WorkflowTransition
1612 WorkflowTransitions_154:
1383 1613 new_status_id: 5
1384 1614 role_id: 3
1385 1615 old_status_id: 1
1386 1616 id: 154
1387 1617 tracker_id: 2
1388 workflows_181:
1618 type: WorkflowTransition
1619 WorkflowTransitions_181:
1389 1620 new_status_id: 2
1390 1621 role_id: 1
1391 1622 old_status_id: 1
1392 1623 id: 181
1393 1624 tracker_id: 3
1394 workflows_209:
1625 type: WorkflowTransition
1626 WorkflowTransitions_209:
1395 1627 new_status_id: 4
1396 1628 role_id: 1
1397 1629 old_status_id: 6
1398 1630 id: 209
1399 1631 tracker_id: 3
1400 workflows_047:
1632 type: WorkflowTransition
1633 WorkflowTransitions_047:
1401 1634 new_status_id: 2
1402 1635 role_id: 2
1403 1636 old_status_id: 4
1404 1637 id: 47
1405 1638 tracker_id: 1
1406 workflows_073:
1639 type: WorkflowTransition
1640 WorkflowTransitions_073:
1407 1641 new_status_id: 4
1408 1642 role_id: 3
1409 1643 old_status_id: 3
1410 1644 id: 73
1411 1645 tracker_id: 1
1412 workflows_128:
1646 type: WorkflowTransition
1647 WorkflowTransitions_128:
1413 1648 new_status_id: 4
1414 1649 role_id: 2
1415 1650 old_status_id: 2
1416 1651 id: 128
1417 1652 tracker_id: 2
1418 workflows_262:
1653 type: WorkflowTransition
1654 WorkflowTransitions_262:
1419 1655 new_status_id: 2
1420 1656 role_id: 3
1421 1657 old_status_id: 5
1422 1658 id: 262
1423 1659 tracker_id: 3
1424 workflows_236:
1660 type: WorkflowTransition
1661 WorkflowTransitions_236:
1425 1662 new_status_id: 1
1426 1663 role_id: 2
1427 1664 old_status_id: 6
1428 1665 id: 236
1429 1666 tracker_id: 3
1430 workflows_155:
1667 type: WorkflowTransition
1668 WorkflowTransitions_155:
1431 1669 new_status_id: 6
1432 1670 role_id: 3
1433 1671 old_status_id: 1
1434 1672 id: 155
1435 1673 tracker_id: 2
1436 workflows_048:
1674 type: WorkflowTransition
1675 WorkflowTransitions_048:
1437 1676 new_status_id: 3
1438 1677 role_id: 2
1439 1678 old_status_id: 4
1440 1679 id: 48
1441 1680 tracker_id: 1
1442 workflows_074:
1681 type: WorkflowTransition
1682 WorkflowTransitions_074:
1443 1683 new_status_id: 5
1444 1684 role_id: 3
1445 1685 old_status_id: 3
1446 1686 id: 74
1447 1687 tracker_id: 1
1448 workflows_129:
1688 type: WorkflowTransition
1689 WorkflowTransitions_129:
1449 1690 new_status_id: 5
1450 1691 role_id: 2
1451 1692 old_status_id: 2
1452 1693 id: 129
1453 1694 tracker_id: 2
1454 workflows_263:
1695 type: WorkflowTransition
1696 WorkflowTransitions_263:
1455 1697 new_status_id: 3
1456 1698 role_id: 3
1457 1699 old_status_id: 5
1458 1700 id: 263
1459 1701 tracker_id: 3
1460 workflows_237:
1702 type: WorkflowTransition
1703 WorkflowTransitions_237:
1461 1704 new_status_id: 2
1462 1705 role_id: 2
1463 1706 old_status_id: 6
1464 1707 id: 237
1465 1708 tracker_id: 3
1466 workflows_182:
1709 type: WorkflowTransition
1710 WorkflowTransitions_182:
1467 1711 new_status_id: 3
1468 1712 role_id: 1
1469 1713 old_status_id: 1
1470 1714 id: 182
1471 1715 tracker_id: 3
1472 workflows_049:
1716 type: WorkflowTransition
1717 WorkflowTransitions_049:
1473 1718 new_status_id: 5
1474 1719 role_id: 2
1475 1720 old_status_id: 4
1476 1721 id: 49
1477 1722 tracker_id: 1
1478 workflows_075:
1723 type: WorkflowTransition
1724 WorkflowTransitions_075:
1479 1725 new_status_id: 6
1480 1726 role_id: 3
1481 1727 old_status_id: 3
1482 1728 id: 75
1483 1729 tracker_id: 1
1484 workflows_156:
1730 type: WorkflowTransition
1731 WorkflowTransitions_156:
1485 1732 new_status_id: 1
1486 1733 role_id: 3
1487 1734 old_status_id: 2
1488 1735 id: 156
1489 1736 tracker_id: 2
1490 workflows_264:
1737 type: WorkflowTransition
1738 WorkflowTransitions_264:
1491 1739 new_status_id: 4
1492 1740 role_id: 3
1493 1741 old_status_id: 5
1494 1742 id: 264
1495 1743 tracker_id: 3
1496 workflows_238:
1744 type: WorkflowTransition
1745 WorkflowTransitions_238:
1497 1746 new_status_id: 3
1498 1747 role_id: 2
1499 1748 old_status_id: 6
1500 1749 id: 238
1501 1750 tracker_id: 3
1502 workflows_183:
1751 type: WorkflowTransition
1752 WorkflowTransitions_183:
1503 1753 new_status_id: 4
1504 1754 role_id: 1
1505 1755 old_status_id: 1
1506 1756 id: 183
1507 1757 tracker_id: 3
1508 workflows_076:
1758 type: WorkflowTransition
1759 WorkflowTransitions_076:
1509 1760 new_status_id: 1
1510 1761 role_id: 3
1511 1762 old_status_id: 4
1512 1763 id: 76
1513 1764 tracker_id: 1
1514 workflows_157:
1765 type: WorkflowTransition
1766 WorkflowTransitions_157:
1515 1767 new_status_id: 3
1516 1768 role_id: 3
1517 1769 old_status_id: 2
1518 1770 id: 157
1519 1771 tracker_id: 2
1520 workflows_265:
1772 type: WorkflowTransition
1773 WorkflowTransitions_265:
1521 1774 new_status_id: 6
1522 1775 role_id: 3
1523 1776 old_status_id: 5
1524 1777 id: 265
1525 1778 tracker_id: 3
1526 workflows_239:
1779 type: WorkflowTransition
1780 WorkflowTransitions_239:
1527 1781 new_status_id: 4
1528 1782 role_id: 2
1529 1783 old_status_id: 6
1530 1784 id: 239
1531 1785 tracker_id: 3
1532 workflows_077:
1786 type: WorkflowTransition
1787 WorkflowTransitions_077:
1533 1788 new_status_id: 2
1534 1789 role_id: 3
1535 1790 old_status_id: 4
1536 1791 id: 77
1537 1792 tracker_id: 1
1538 workflows_158:
1793 type: WorkflowTransition
1794 WorkflowTransitions_158:
1539 1795 new_status_id: 4
1540 1796 role_id: 3
1541 1797 old_status_id: 2
1542 1798 id: 158
1543 1799 tracker_id: 2
1544 workflows_184:
1800 type: WorkflowTransition
1801 WorkflowTransitions_184:
1545 1802 new_status_id: 5
1546 1803 role_id: 1
1547 1804 old_status_id: 1
1548 1805 id: 184
1549 1806 tracker_id: 3
1550 workflows_266:
1807 type: WorkflowTransition
1808 WorkflowTransitions_266:
1551 1809 new_status_id: 1
1552 1810 role_id: 3
1553 1811 old_status_id: 6
1554 1812 id: 266
1555 1813 tracker_id: 3
1556 workflows_078:
1814 type: WorkflowTransition
1815 WorkflowTransitions_078:
1557 1816 new_status_id: 3
1558 1817 role_id: 3
1559 1818 old_status_id: 4
1560 1819 id: 78
1561 1820 tracker_id: 1
1562 workflows_159:
1821 type: WorkflowTransition
1822 WorkflowTransitions_159:
1563 1823 new_status_id: 5
1564 1824 role_id: 3
1565 1825 old_status_id: 2
1566 1826 id: 159
1567 1827 tracker_id: 2
1568 workflows_185:
1828 type: WorkflowTransition
1829 WorkflowTransitions_185:
1569 1830 new_status_id: 6
1570 1831 role_id: 1
1571 1832 old_status_id: 1
1572 1833 id: 185
1573 1834 tracker_id: 3
1574 workflows_267:
1835 type: WorkflowTransition
1836 WorkflowTransitions_267:
1575 1837 new_status_id: 2
1576 1838 role_id: 3
1577 1839 old_status_id: 6
1578 1840 id: 267
1579 1841 tracker_id: 3
1580 workflows_079:
1842 type: WorkflowTransition
1843 WorkflowTransitions_079:
1581 1844 new_status_id: 5
1582 1845 role_id: 3
1583 1846 old_status_id: 4
1584 1847 id: 79
1585 1848 tracker_id: 1
1586 workflows_186:
1849 type: WorkflowTransition
1850 WorkflowTransitions_186:
1587 1851 new_status_id: 1
1588 1852 role_id: 1
1589 1853 old_status_id: 2
1590 1854 id: 186
1591 1855 tracker_id: 3
1592 workflows_268:
1856 type: WorkflowTransition
1857 WorkflowTransitions_268:
1593 1858 new_status_id: 3
1594 1859 role_id: 3
1595 1860 old_status_id: 6
1596 1861 id: 268
1597 1862 tracker_id: 3
1598 workflows_187:
1863 type: WorkflowTransition
1864 WorkflowTransitions_187:
1599 1865 new_status_id: 3
1600 1866 role_id: 1
1601 1867 old_status_id: 2
1602 1868 id: 187
1603 1869 tracker_id: 3
1604 workflows_269:
1870 type: WorkflowTransition
1871 WorkflowTransitions_269:
1605 1872 new_status_id: 4
1606 1873 role_id: 3
1607 1874 old_status_id: 6
1608 1875 id: 269
1609 1876 tracker_id: 3
1610 workflows_188:
1877 type: WorkflowTransition
1878 WorkflowTransitions_188:
1611 1879 new_status_id: 4
1612 1880 role_id: 1
1613 1881 old_status_id: 2
1614 1882 id: 188
1615 1883 tracker_id: 3
1884 type: WorkflowTransition
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,186 +1,186
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 RolesControllerTest < ActionController::TestCase
21 21 fixtures :roles, :users, :members, :member_roles, :workflows, :trackers
22 22
23 23 def setup
24 24 @controller = RolesController.new
25 25 @request = ActionController::TestRequest.new
26 26 @response = ActionController::TestResponse.new
27 27 User.current = nil
28 28 @request.session[:user_id] = 1 # admin
29 29 end
30 30
31 31 def test_index
32 32 get :index
33 33 assert_response :success
34 34 assert_template 'index'
35 35
36 36 assert_not_nil assigns(:roles)
37 37 assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
38 38
39 39 assert_tag :tag => 'a', :attributes => { :href => '/roles/1/edit' },
40 40 :content => 'Manager'
41 41 end
42 42
43 43 def test_new
44 44 get :new
45 45 assert_response :success
46 46 assert_template 'new'
47 47 end
48 48
49 49 def test_create_with_validaton_failure
50 50 post :create, :role => {:name => '',
51 51 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
52 52 :assignable => '0'}
53 53
54 54 assert_response :success
55 55 assert_template 'new'
56 56 assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' }
57 57 end
58 58
59 59 def test_create_without_workflow_copy
60 60 post :create, :role => {:name => 'RoleWithoutWorkflowCopy',
61 61 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
62 62 :assignable => '0'}
63 63
64 64 assert_redirected_to '/roles'
65 65 role = Role.find_by_name('RoleWithoutWorkflowCopy')
66 66 assert_not_nil role
67 67 assert_equal [:add_issues, :edit_issues, :log_time], role.permissions
68 68 assert !role.assignable?
69 69 end
70 70
71 71 def test_create_with_workflow_copy
72 72 post :create, :role => {:name => 'RoleWithWorkflowCopy',
73 73 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
74 74 :assignable => '0'},
75 75 :copy_workflow_from => '1'
76 76
77 77 assert_redirected_to '/roles'
78 78 role = Role.find_by_name('RoleWithWorkflowCopy')
79 79 assert_not_nil role
80 assert_equal Role.find(1).workflows.size, role.workflows.size
80 assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size
81 81 end
82 82
83 83 def test_edit
84 84 get :edit, :id => 1
85 85 assert_response :success
86 86 assert_template 'edit'
87 87 assert_equal Role.find(1), assigns(:role)
88 88 end
89 89
90 90 def test_edit_invalid_should_respond_with_404
91 91 get :edit, :id => 999
92 92 assert_response 404
93 93 end
94 94
95 95 def test_update
96 96 put :update, :id => 1,
97 97 :role => {:name => 'Manager',
98 98 :permissions => ['edit_project', ''],
99 99 :assignable => '0'}
100 100
101 101 assert_redirected_to '/roles'
102 102 role = Role.find(1)
103 103 assert_equal [:edit_project], role.permissions
104 104 end
105 105
106 106 def test_update_with_failure
107 107 put :update, :id => 1, :role => {:name => ''}
108 108 assert_response :success
109 109 assert_template 'edit'
110 110 end
111 111
112 112 def test_destroy
113 113 r = Role.create!(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages])
114 114
115 115 delete :destroy, :id => r
116 116 assert_redirected_to '/roles'
117 117 assert_nil Role.find_by_id(r.id)
118 118 end
119 119
120 120 def test_destroy_role_in_use
121 121 delete :destroy, :id => 1
122 122 assert_redirected_to '/roles'
123 123 assert_equal 'This role is in use and cannot be deleted.', flash[:error]
124 124 assert_not_nil Role.find_by_id(1)
125 125 end
126 126
127 127 def test_get_permissions
128 128 get :permissions
129 129 assert_response :success
130 130 assert_template 'permissions'
131 131
132 132 assert_not_nil assigns(:roles)
133 133 assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
134 134
135 135 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
136 136 :name => 'permissions[3][]',
137 137 :value => 'add_issues',
138 138 :checked => 'checked' }
139 139
140 140 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
141 141 :name => 'permissions[3][]',
142 142 :value => 'delete_issues',
143 143 :checked => nil }
144 144 end
145 145
146 146 def test_post_permissions
147 147 post :permissions, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']}
148 148 assert_redirected_to '/roles'
149 149
150 150 assert_equal [:edit_issues], Role.find(1).permissions
151 151 assert_equal [:add_issues, :delete_issues], Role.find(3).permissions
152 152 assert Role.find(2).permissions.empty?
153 153 end
154 154
155 155 def test_clear_all_permissions
156 156 post :permissions, :permissions => { '0' => '' }
157 157 assert_redirected_to '/roles'
158 158 assert Role.find(1).permissions.empty?
159 159 end
160 160
161 161 def test_move_highest
162 162 put :update, :id => 3, :role => {:move_to => 'highest'}
163 163 assert_redirected_to '/roles'
164 164 assert_equal 1, Role.find(3).position
165 165 end
166 166
167 167 def test_move_higher
168 168 position = Role.find(3).position
169 169 put :update, :id => 3, :role => {:move_to => 'higher'}
170 170 assert_redirected_to '/roles'
171 171 assert_equal position - 1, Role.find(3).position
172 172 end
173 173
174 174 def test_move_lower
175 175 position = Role.find(2).position
176 176 put :update, :id => 2, :role => {:move_to => 'lower'}
177 177 assert_redirected_to '/roles'
178 178 assert_equal position + 1, Role.find(2).position
179 179 end
180 180
181 181 def test_move_lowest
182 182 put :update, :id => 2, :role => {:move_to => 'lowest'}
183 183 assert_redirected_to '/roles'
184 184 assert_equal Role.count, Role.find(2).position
185 185 end
186 186 end
@@ -1,188 +1,188
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19 require 'trackers_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class TrackersController; def rescue_action(e) raise e end; end
23 23
24 24 class TrackersControllerTest < ActionController::TestCase
25 25 fixtures :trackers, :projects, :projects_trackers, :users, :issues, :custom_fields
26 26
27 27 def setup
28 28 @controller = TrackersController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 @request.session[:user_id] = 1 # admin
33 33 end
34 34
35 35 def test_index
36 36 get :index
37 37 assert_response :success
38 38 assert_template 'index'
39 39 end
40 40
41 41 def test_index_by_anonymous_should_redirect_to_login_form
42 42 @request.session[:user_id] = nil
43 43 get :index
44 44 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftrackers'
45 45 end
46 46
47 47 def test_index_by_user_should_respond_with_406
48 48 @request.session[:user_id] = 2
49 49 get :index
50 50 assert_response 406
51 51 end
52 52
53 53 def test_new
54 54 get :new
55 55 assert_response :success
56 56 assert_template 'new'
57 57 end
58 58
59 59 def test_create
60 60 assert_difference 'Tracker.count' do
61 61 post :create, :tracker => { :name => 'New tracker', :project_ids => ['1', '', ''], :custom_field_ids => ['1', '6', ''] }
62 62 end
63 63 assert_redirected_to :action => 'index'
64 64 tracker = Tracker.first(:order => 'id DESC')
65 65 assert_equal 'New tracker', tracker.name
66 66 assert_equal [1], tracker.project_ids.sort
67 67 assert_equal Tracker::CORE_FIELDS, tracker.core_fields
68 68 assert_equal [1, 6], tracker.custom_field_ids.sort
69 assert_equal 0, tracker.workflows.count
69 assert_equal 0, tracker.workflow_rules.count
70 70 end
71 71
72 72 def create_with_disabled_core_fields
73 73 assert_difference 'Tracker.count' do
74 74 post :create, :tracker => { :name => 'New tracker', :core_fields => ['assigned_to_id', 'fixed_version_id', ''] }
75 75 end
76 76 assert_redirected_to :action => 'index'
77 77 tracker = Tracker.first(:order => 'id DESC')
78 78 assert_equal 'New tracker', tracker.name
79 79 assert_equal %w(assigned_to_id fixed_version_id), tracker.core_fields
80 80 end
81 81
82 82 def test_create_new_with_workflow_copy
83 83 assert_difference 'Tracker.count' do
84 84 post :create, :tracker => { :name => 'New tracker' }, :copy_workflow_from => 1
85 85 end
86 86 assert_redirected_to :action => 'index'
87 87 tracker = Tracker.find_by_name('New tracker')
88 88 assert_equal 0, tracker.projects.count
89 assert_equal Tracker.find(1).workflows.count, tracker.workflows.count
89 assert_equal Tracker.find(1).workflow_rules.count, tracker.workflow_rules.count
90 90 end
91 91
92 92 def test_create_with_failure
93 93 assert_no_difference 'Tracker.count' do
94 94 post :create, :tracker => { :name => '', :project_ids => ['1', '', ''], :custom_field_ids => ['1', '6', ''] }
95 95 end
96 96 assert_response :success
97 97 assert_template 'new'
98 98 assert_error_tag :content => /name can't be blank/i
99 99 end
100 100
101 101 def test_edit
102 102 Tracker.find(1).project_ids = [1, 3]
103 103
104 104 get :edit, :id => 1
105 105 assert_response :success
106 106 assert_template 'edit'
107 107
108 108 assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
109 109 :value => '1',
110 110 :checked => 'checked' }
111 111
112 112 assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
113 113 :value => '2',
114 114 :checked => nil }
115 115
116 116 assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
117 117 :value => '',
118 118 :type => 'hidden'}
119 119 end
120 120
121 121 def test_edit_should_check_core_fields
122 122 tracker = Tracker.find(1)
123 123 tracker.core_fields = %w(assigned_to_id fixed_version_id)
124 124 tracker.save!
125 125
126 126 get :edit, :id => 1
127 127 assert_response :success
128 128 assert_template 'edit'
129 129
130 130 assert_select 'input[name=?][value=assigned_to_id][checked=checked]', 'tracker[core_fields][]'
131 131 assert_select 'input[name=?][value=fixed_version_id][checked=checked]', 'tracker[core_fields][]'
132 132
133 133 assert_select 'input[name=?][value=category_id]', 'tracker[core_fields][]'
134 134 assert_select 'input[name=?][value=category_id][checked=checked]', 'tracker[core_fields][]', 0
135 135
136 136 assert_select 'input[name=?][value=][type=hidden]', 'tracker[core_fields][]'
137 137 end
138 138
139 139 def test_update
140 140 put :update, :id => 1, :tracker => { :name => 'Renamed',
141 141 :project_ids => ['1', '2', ''] }
142 142 assert_redirected_to :action => 'index'
143 143 assert_equal [1, 2], Tracker.find(1).project_ids.sort
144 144 end
145 145
146 146 def test_update_without_projects
147 147 put :update, :id => 1, :tracker => { :name => 'Renamed',
148 148 :project_ids => [''] }
149 149 assert_redirected_to :action => 'index'
150 150 assert Tracker.find(1).project_ids.empty?
151 151 end
152 152
153 153 def test_update_without_core_fields
154 154 put :update, :id => 1, :tracker => { :name => 'Renamed', :core_fields => [''] }
155 155 assert_redirected_to :action => 'index'
156 156 assert Tracker.find(1).core_fields.empty?
157 157 end
158 158
159 159 def test_update_with_failure
160 160 put :update, :id => 1, :tracker => { :name => '' }
161 161 assert_response :success
162 162 assert_template 'edit'
163 163 assert_error_tag :content => /name can't be blank/i
164 164 end
165 165
166 166 def test_move_lower
167 167 tracker = Tracker.find_by_position(1)
168 168 put :update, :id => 1, :tracker => { :move_to => 'lower' }
169 169 assert_equal 2, tracker.reload.position
170 170 end
171 171
172 172 def test_destroy
173 173 tracker = Tracker.create!(:name => 'Destroyable')
174 174 assert_difference 'Tracker.count', -1 do
175 175 delete :destroy, :id => tracker.id
176 176 end
177 177 assert_redirected_to :action => 'index'
178 178 assert_nil flash[:error]
179 179 end
180 180
181 181 def test_destroy_tracker_in_use
182 182 assert_no_difference 'Tracker.count' do
183 183 delete :destroy, :id => 1
184 184 end
185 185 assert_redirected_to :action => 'index'
186 186 assert_not_nil flash[:error]
187 187 end
188 188 end
@@ -1,198 +1,288
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19 require 'workflows_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class WorkflowsController; def rescue_action(e) raise e end; end
23 23
24 24 class WorkflowsControllerTest < ActionController::TestCase
25 25 fixtures :roles, :trackers, :workflows, :users, :issue_statuses
26 26
27 27 def setup
28 28 @controller = WorkflowsController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 @request.session[:user_id] = 1 # admin
33 33 end
34 34
35 35 def test_index
36 36 get :index
37 37 assert_response :success
38 38 assert_template 'index'
39 39
40 count = Workflow.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2')
40 count = WorkflowTransition.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2')
41 41 assert_tag :tag => 'a', :content => count.to_s,
42 42 :attributes => { :href => '/workflows/edit?role_id=1&amp;tracker_id=2' }
43 43 end
44 44
45 45 def test_get_edit
46 46 get :edit
47 47 assert_response :success
48 48 assert_template 'edit'
49 49 assert_not_nil assigns(:roles)
50 50 assert_not_nil assigns(:trackers)
51 51 end
52 52
53 53 def test_get_edit_with_role_and_tracker
54 Workflow.delete_all
55 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
56 Workflow.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
54 WorkflowTransition.delete_all
55 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
56 WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
57 57
58 58 get :edit, :role_id => 2, :tracker_id => 1
59 59 assert_response :success
60 60 assert_template 'edit'
61 61
62 62 # used status only
63 63 assert_not_nil assigns(:statuses)
64 64 assert_equal [2, 3, 5], assigns(:statuses).collect(&:id)
65 65
66 66 # allowed transitions
67 67 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
68 68 :name => 'issue_status[3][5][]',
69 69 :value => 'always',
70 70 :checked => 'checked' }
71 71 # not allowed
72 72 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
73 73 :name => 'issue_status[3][2][]',
74 74 :value => 'always',
75 75 :checked => nil }
76 76 # unused
77 77 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
78 78 :name => 'issue_status[1][1][]' }
79 79 end
80 80
81 81 def test_get_edit_with_role_and_tracker_and_all_statuses
82 Workflow.delete_all
82 WorkflowTransition.delete_all
83 83
84 84 get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0'
85 85 assert_response :success
86 86 assert_template 'edit'
87 87
88 88 assert_not_nil assigns(:statuses)
89 89 assert_equal IssueStatus.count, assigns(:statuses).size
90 90
91 91 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
92 92 :name => 'issue_status[1][1][]',
93 93 :value => 'always',
94 94 :checked => nil }
95 95 end
96 96
97 97 def test_post_edit
98 98 post :edit, :role_id => 2, :tracker_id => 1,
99 99 :issue_status => {
100 100 '4' => {'5' => ['always']},
101 101 '3' => {'1' => ['always'], '2' => ['always']}
102 102 }
103 103 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
104 104
105 assert_equal 3, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
106 assert_not_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
107 assert_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4})
105 assert_equal 3, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2})
106 assert_not_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
107 assert_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4})
108 108 end
109 109
110 110 def test_post_edit_with_additional_transitions
111 111 post :edit, :role_id => 2, :tracker_id => 1,
112 112 :issue_status => {
113 113 '4' => {'5' => ['always']},
114 114 '3' => {'1' => ['author'], '2' => ['assignee'], '4' => ['author', 'assignee']}
115 115 }
116 116 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
117 117
118 assert_equal 4, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
118 assert_equal 4, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2})
119 119
120 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5})
120 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5})
121 121 assert ! w.author
122 122 assert ! w.assignee
123 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1})
123 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1})
124 124 assert w.author
125 125 assert ! w.assignee
126 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
126 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
127 127 assert ! w.author
128 128 assert w.assignee
129 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4})
129 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4})
130 130 assert w.author
131 131 assert w.assignee
132 132 end
133 133
134 134 def test_clear_workflow
135 assert Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0
135 assert WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0
136 136
137 137 post :edit, :role_id => 2, :tracker_id => 1
138 assert_equal 0, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
138 assert_equal 0, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2})
139 end
140
141 def test_get_permissions
142 get :permissions
143
144 assert_response :success
145 assert_template 'permissions'
146 assert_not_nil assigns(:roles)
147 assert_not_nil assigns(:trackers)
148 end
149
150 def test_get_permissions_with_role_and_tracker
151 WorkflowPermission.delete_all
152 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
153 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
154 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
155
156 get :permissions, :role_id => 1, :tracker_id => 2
157 assert_response :success
158 assert_template 'permissions'
159
160 assert_select 'input[name=role_id][value=1]'
161 assert_select 'input[name=tracker_id][value=2]'
162
163 # Required field
164 assert_select 'select[name=?]', 'permissions[assigned_to_id][2]' do
165 assert_select 'option[value=]'
166 assert_select 'option[value=][selected=selected]', 0
167 assert_select 'option[value=readonly]', :text => 'Read-only'
168 assert_select 'option[value=readonly][selected=selected]', 0
169 assert_select 'option[value=required]', :text => 'Required'
170 assert_select 'option[value=required][selected=selected]'
171 end
172
173 # Read-only field
174 assert_select 'select[name=?]', 'permissions[fixed_version_id][3]' do
175 assert_select 'option[value=]'
176 assert_select 'option[value=][selected=selected]', 0
177 assert_select 'option[value=readonly]', :text => 'Read-only'
178 assert_select 'option[value=readonly][selected=selected]'
179 assert_select 'option[value=required]', :text => 'Required'
180 assert_select 'option[value=required][selected=selected]', 0
181 end
182
183 # Other field
184 assert_select 'select[name=?]', 'permissions[due_date][3]' do
185 assert_select 'option[value=]'
186 assert_select 'option[value=][selected=selected]', 0
187 assert_select 'option[value=readonly]', :text => 'Read-only'
188 assert_select 'option[value=readonly][selected=selected]', 0
189 assert_select 'option[value=required]', :text => 'Required'
190 assert_select 'option[value=required][selected=selected]', 0
191 end
192 end
193
194 def test_post_permissions
195 WorkflowPermission.delete_all
196
197 post :permissions, :role_id => 1, :tracker_id => 2, :permissions => {
198 'assigned_to_id' => {'1' => '', '2' => 'readonly', '3' => ''},
199 'fixed_version_id' => {'1' => 'required', '2' => 'readonly', '3' => ''},
200 'due_date' => {'1' => '', '2' => '', '3' => ''},
201 }
202 assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2'
203
204 workflows = WorkflowPermission.all
205 assert_equal 3, workflows.size
206 workflows.each do |workflow|
207 assert_equal 1, workflow.role_id
208 assert_equal 2, workflow.tracker_id
209 end
210 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'assigned_to_id' && wf.rule == 'readonly'}
211 assert workflows.detect {|wf| wf.old_status_id == 1 && wf.field_name == 'fixed_version_id' && wf.rule == 'required'}
212 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'fixed_version_id' && wf.rule == 'readonly'}
213 end
214
215 def test_post_permissions_should_clear_permissions
216 WorkflowPermission.delete_all
217 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
218 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
219 wf1 = WorkflowPermission.create!(:role_id => 1, :tracker_id => 3, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
220 wf2 = WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
221
222 post :permissions, :role_id => 1, :tracker_id => 2
223 assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2'
224
225 workflows = WorkflowPermission.all
226 assert_equal 2, workflows.size
227 assert wf1.reload
228 assert wf2.reload
139 229 end
140 230
141 231 def test_get_copy
142 232 get :copy
143 233 assert_response :success
144 234 assert_template 'copy'
145 235 assert_select 'select[name=source_tracker_id]' do
146 236 assert_select 'option[value=1]', :text => 'Bug'
147 237 end
148 238 assert_select 'select[name=source_role_id]' do
149 239 assert_select 'option[value=2]', :text => 'Developer'
150 240 end
151 241 assert_select 'select[name=?]', 'target_tracker_ids[]' do
152 242 assert_select 'option[value=3]', :text => 'Support request'
153 243 end
154 244 assert_select 'select[name=?]', 'target_role_ids[]' do
155 245 assert_select 'option[value=1]', :text => 'Manager'
156 246 end
157 247 end
158 248
159 249 def test_post_copy_one_to_one
160 250 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
161 251
162 252 post :copy, :source_tracker_id => '1', :source_role_id => '2',
163 253 :target_tracker_ids => ['3'], :target_role_ids => ['1']
164 254 assert_response 302
165 255 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
166 256 end
167 257
168 258 def test_post_copy_one_to_many
169 259 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
170 260
171 261 post :copy, :source_tracker_id => '1', :source_role_id => '2',
172 262 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
173 263 assert_response 302
174 264 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1)
175 265 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
176 266 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3)
177 267 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3)
178 268 end
179 269
180 270 def test_post_copy_many_to_many
181 271 source_t2 = status_transitions(:tracker_id => 2, :role_id => 2)
182 272 source_t3 = status_transitions(:tracker_id => 3, :role_id => 2)
183 273
184 274 post :copy, :source_tracker_id => 'any', :source_role_id => '2',
185 275 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
186 276 assert_response 302
187 277 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1)
188 278 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1)
189 279 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3)
190 280 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3)
191 281 end
192 282
193 283 # Returns an array of status transitions that can be compared
194 284 def status_transitions(conditions)
195 Workflow.find(:all, :conditions => conditions,
285 WorkflowTransition.find(:all, :conditions => conditions,
196 286 :order => 'tracker_id, role_id, old_status_id, new_status_id').collect {|w| [w.old_status, w.new_status_id]}
197 287 end
198 288 end
@@ -1,114 +1,114
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssueStatusTest < ActiveSupport::TestCase
21 21 fixtures :issue_statuses, :issues, :roles, :trackers
22 22
23 23 def test_create
24 24 status = IssueStatus.new :name => "Assigned"
25 25 assert !status.save
26 26 # status name uniqueness
27 27 assert_equal 1, status.errors.count
28 28
29 29 status.name = "Test Status"
30 30 assert status.save
31 31 assert !status.is_default
32 32 end
33 33
34 34 def test_destroy
35 35 status = IssueStatus.find(3)
36 36 assert_difference 'IssueStatus.count', -1 do
37 37 assert status.destroy
38 38 end
39 assert_nil Workflow.first(:conditions => {:old_status_id => status.id})
40 assert_nil Workflow.first(:conditions => {:new_status_id => status.id})
39 assert_nil WorkflowTransition.first(:conditions => {:old_status_id => status.id})
40 assert_nil WorkflowTransition.first(:conditions => {:new_status_id => status.id})
41 41 end
42 42
43 43 def test_destroy_status_in_use
44 44 # Status assigned to an Issue
45 45 status = Issue.find(1).status
46 46 assert_raise(RuntimeError, "Can't delete status") { status.destroy }
47 47 end
48 48
49 49 def test_default
50 50 status = IssueStatus.default
51 51 assert_kind_of IssueStatus, status
52 52 end
53 53
54 54 def test_change_default
55 55 status = IssueStatus.find(2)
56 56 assert !status.is_default
57 57 status.is_default = true
58 58 assert status.save
59 59 status.reload
60 60
61 61 assert_equal status, IssueStatus.default
62 62 assert !IssueStatus.find(1).is_default
63 63 end
64 64
65 65 def test_reorder_should_not_clear_default_status
66 66 status = IssueStatus.default
67 67 status.move_to_bottom
68 68 status.reload
69 69 assert status.is_default?
70 70 end
71 71
72 72 def test_new_statuses_allowed_to
73 Workflow.delete_all
73 WorkflowTransition.delete_all
74 74
75 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
76 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
77 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
78 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
75 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
76 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
77 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
78 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
79 79 status = IssueStatus.find(1)
80 80 role = Role.find(1)
81 81 tracker = Tracker.find(1)
82 82
83 83 assert_equal [2], status.new_statuses_allowed_to([role], tracker, false, false).map(&:id)
84 84 assert_equal [2], status.find_new_statuses_allowed_to([role], tracker, false, false).map(&:id)
85 85
86 86 assert_equal [2, 3, 5], status.new_statuses_allowed_to([role], tracker, true, false).map(&:id)
87 87 assert_equal [2, 3, 5], status.find_new_statuses_allowed_to([role], tracker, true, false).map(&:id)
88 88
89 89 assert_equal [2, 4, 5], status.new_statuses_allowed_to([role], tracker, false, true).map(&:id)
90 90 assert_equal [2, 4, 5], status.find_new_statuses_allowed_to([role], tracker, false, true).map(&:id)
91 91
92 92 assert_equal [2, 3, 4, 5], status.new_statuses_allowed_to([role], tracker, true, true).map(&:id)
93 93 assert_equal [2, 3, 4, 5], status.find_new_statuses_allowed_to([role], tracker, true, true).map(&:id)
94 94 end
95 95
96 96 def test_update_done_ratios_with_issue_done_ratio_set_to_issue_field_should_change_nothing
97 97 IssueStatus.find(1).update_attribute(:default_done_ratio, 50)
98 98
99 99 with_settings :issue_done_ratio => 'issue_field' do
100 100 IssueStatus.update_issue_done_ratios
101 101 assert_equal 0, Issue.count(:conditions => {:done_ratio => 50})
102 102 end
103 103 end
104 104
105 105 def test_update_done_ratios_with_issue_done_ratio_set_to_issue_status_should_update_issues
106 106 IssueStatus.find(1).update_attribute(:default_done_ratio, 50)
107 107
108 108 with_settings :issue_done_ratio => 'issue_status' do
109 109 IssueStatus.update_issue_done_ratios
110 110 issues = Issue.all(:conditions => {:status_id => 1})
111 111 assert_equal [50], issues.map {|issue| issue.read_attribute(:done_ratio)}.uniq
112 112 end
113 113 end
114 114 end
@@ -1,1350 +1,1513
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssueTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles,
22 22 :groups_users,
23 23 :trackers, :projects_trackers,
24 24 :enabled_modules,
25 25 :versions,
26 26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 27 :enumerations,
28 28 :issues,
29 29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 30 :time_entries
31 31
32 32 include Redmine::I18n
33 33
34 def teardown
35 User.current = nil
36 end
37
34 38 def test_create
35 39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
36 40 :status_id => 1, :priority => IssuePriority.all.first,
37 41 :subject => 'test_create',
38 42 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
39 43 assert issue.save
40 44 issue.reload
41 45 assert_equal 1.5, issue.estimated_hours
42 46 end
43 47
44 48 def test_create_minimal
45 49 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
46 50 :status_id => 1, :priority => IssuePriority.all.first,
47 51 :subject => 'test_create')
48 52 assert issue.save
49 53 assert issue.description.nil?
50 54 end
51 55
52 56 def test_create_with_required_custom_field
53 57 set_language_if_valid 'en'
54 58 field = IssueCustomField.find_by_name('Database')
55 59 field.update_attribute(:is_required, true)
56 60
57 61 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
58 62 :status_id => 1, :subject => 'test_create',
59 63 :description => 'IssueTest#test_create_with_required_custom_field')
60 64 assert issue.available_custom_fields.include?(field)
61 65 # No value for the custom field
62 66 assert !issue.save
63 67 assert_equal ["Database can't be blank"], issue.errors.full_messages
64 68 # Blank value
65 69 issue.custom_field_values = { field.id => '' }
66 70 assert !issue.save
67 71 assert_equal ["Database can't be blank"], issue.errors.full_messages
68 72 # Invalid value
69 73 issue.custom_field_values = { field.id => 'SQLServer' }
70 74 assert !issue.save
71 75 assert_equal ["Database is not included in the list"], issue.errors.full_messages
72 76 # Valid value
73 77 issue.custom_field_values = { field.id => 'PostgreSQL' }
74 78 assert issue.save
75 79 issue.reload
76 80 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
77 81 end
78 82
79 83 def test_create_with_group_assignment
80 84 with_settings :issue_group_assignment => '1' do
81 85 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
82 86 :subject => 'Group assignment',
83 87 :assigned_to_id => 11).save
84 88 issue = Issue.first(:order => 'id DESC')
85 89 assert_kind_of Group, issue.assigned_to
86 90 assert_equal Group.find(11), issue.assigned_to
87 91 end
88 92 end
89 93
90 94 def assert_visibility_match(user, issues)
91 95 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
92 96 end
93 97
94 98 def test_visible_scope_for_anonymous
95 99 # Anonymous user should see issues of public projects only
96 100 issues = Issue.visible(User.anonymous).all
97 101 assert issues.any?
98 102 assert_nil issues.detect {|issue| !issue.project.is_public?}
99 103 assert_nil issues.detect {|issue| issue.is_private?}
100 104 assert_visibility_match User.anonymous, issues
101 105 end
102 106
103 107 def test_visible_scope_for_anonymous_with_own_issues_visibility
104 108 Role.anonymous.update_attribute :issues_visibility, 'own'
105 109 Issue.create!(:project_id => 1, :tracker_id => 1,
106 110 :author_id => User.anonymous.id,
107 111 :subject => 'Issue by anonymous')
108 112
109 113 issues = Issue.visible(User.anonymous).all
110 114 assert issues.any?
111 115 assert_nil issues.detect {|issue| issue.author != User.anonymous}
112 116 assert_visibility_match User.anonymous, issues
113 117 end
114 118
115 119 def test_visible_scope_for_anonymous_without_view_issues_permissions
116 120 # Anonymous user should not see issues without permission
117 121 Role.anonymous.remove_permission!(:view_issues)
118 122 issues = Issue.visible(User.anonymous).all
119 123 assert issues.empty?
120 124 assert_visibility_match User.anonymous, issues
121 125 end
122 126
123 127 def test_visible_scope_for_non_member
124 128 user = User.find(9)
125 129 assert user.projects.empty?
126 130 # Non member user should see issues of public projects only
127 131 issues = Issue.visible(user).all
128 132 assert issues.any?
129 133 assert_nil issues.detect {|issue| !issue.project.is_public?}
130 134 assert_nil issues.detect {|issue| issue.is_private?}
131 135 assert_visibility_match user, issues
132 136 end
133 137
134 138 def test_visible_scope_for_non_member_with_own_issues_visibility
135 139 Role.non_member.update_attribute :issues_visibility, 'own'
136 140 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
137 141 user = User.find(9)
138 142
139 143 issues = Issue.visible(user).all
140 144 assert issues.any?
141 145 assert_nil issues.detect {|issue| issue.author != user}
142 146 assert_visibility_match user, issues
143 147 end
144 148
145 149 def test_visible_scope_for_non_member_without_view_issues_permissions
146 150 # Non member user should not see issues without permission
147 151 Role.non_member.remove_permission!(:view_issues)
148 152 user = User.find(9)
149 153 assert user.projects.empty?
150 154 issues = Issue.visible(user).all
151 155 assert issues.empty?
152 156 assert_visibility_match user, issues
153 157 end
154 158
155 159 def test_visible_scope_for_member
156 160 user = User.find(9)
157 161 # User should see issues of projects for which he has view_issues permissions only
158 162 Role.non_member.remove_permission!(:view_issues)
159 163 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
160 164 issues = Issue.visible(user).all
161 165 assert issues.any?
162 166 assert_nil issues.detect {|issue| issue.project_id != 3}
163 167 assert_nil issues.detect {|issue| issue.is_private?}
164 168 assert_visibility_match user, issues
165 169 end
166 170
167 171 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
168 172 user = User.find(8)
169 173 assert user.groups.any?
170 174 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
171 175 Role.non_member.remove_permission!(:view_issues)
172 176
173 177 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
174 178 :status_id => 1, :priority => IssuePriority.all.first,
175 179 :subject => 'Assignment test',
176 180 :assigned_to => user.groups.first,
177 181 :is_private => true)
178 182
179 183 Role.find(2).update_attribute :issues_visibility, 'default'
180 184 issues = Issue.visible(User.find(8)).all
181 185 assert issues.any?
182 186 assert issues.include?(issue)
183 187
184 188 Role.find(2).update_attribute :issues_visibility, 'own'
185 189 issues = Issue.visible(User.find(8)).all
186 190 assert issues.any?
187 191 assert issues.include?(issue)
188 192 end
189 193
190 194 def test_visible_scope_for_admin
191 195 user = User.find(1)
192 196 user.members.each(&:destroy)
193 197 assert user.projects.empty?
194 198 issues = Issue.visible(user).all
195 199 assert issues.any?
196 200 # Admin should see issues on private projects that he does not belong to
197 201 assert issues.detect {|issue| !issue.project.is_public?}
198 202 # Admin should see private issues of other users
199 203 assert issues.detect {|issue| issue.is_private? && issue.author != user}
200 204 assert_visibility_match user, issues
201 205 end
202 206
203 207 def test_visible_scope_with_project
204 208 project = Project.find(1)
205 209 issues = Issue.visible(User.find(2), :project => project).all
206 210 projects = issues.collect(&:project).uniq
207 211 assert_equal 1, projects.size
208 212 assert_equal project, projects.first
209 213 end
210 214
211 215 def test_visible_scope_with_project_and_subprojects
212 216 project = Project.find(1)
213 217 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
214 218 projects = issues.collect(&:project).uniq
215 219 assert projects.size > 1
216 220 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
217 221 end
218 222
219 223 def test_visible_and_nested_set_scopes
220 224 assert_equal 0, Issue.find(1).descendants.visible.all.size
221 225 end
222 226
223 227 def test_open_scope
224 228 issues = Issue.open.all
225 229 assert_nil issues.detect(&:closed?)
226 230 end
227 231
228 232 def test_open_scope_with_arg
229 233 issues = Issue.open(false).all
230 234 assert_equal issues, issues.select(&:closed?)
231 235 end
232 236
233 237 def test_errors_full_messages_should_include_custom_fields_errors
234 238 field = IssueCustomField.find_by_name('Database')
235 239
236 240 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
237 241 :status_id => 1, :subject => 'test_create',
238 242 :description => 'IssueTest#test_create_with_required_custom_field')
239 243 assert issue.available_custom_fields.include?(field)
240 244 # Invalid value
241 245 issue.custom_field_values = { field.id => 'SQLServer' }
242 246
243 247 assert !issue.valid?
244 248 assert_equal 1, issue.errors.full_messages.size
245 249 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
246 250 issue.errors.full_messages.first
247 251 end
248 252
249 253 def test_update_issue_with_required_custom_field
250 254 field = IssueCustomField.find_by_name('Database')
251 255 field.update_attribute(:is_required, true)
252 256
253 257 issue = Issue.find(1)
254 258 assert_nil issue.custom_value_for(field)
255 259 assert issue.available_custom_fields.include?(field)
256 260 # No change to custom values, issue can be saved
257 261 assert issue.save
258 262 # Blank value
259 263 issue.custom_field_values = { field.id => '' }
260 264 assert !issue.save
261 265 # Valid value
262 266 issue.custom_field_values = { field.id => 'PostgreSQL' }
263 267 assert issue.save
264 268 issue.reload
265 269 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
266 270 end
267 271
268 272 def test_should_not_update_attributes_if_custom_fields_validation_fails
269 273 issue = Issue.find(1)
270 274 field = IssueCustomField.find_by_name('Database')
271 275 assert issue.available_custom_fields.include?(field)
272 276
273 277 issue.custom_field_values = { field.id => 'Invalid' }
274 278 issue.subject = 'Should be not be saved'
275 279 assert !issue.save
276 280
277 281 issue.reload
278 282 assert_equal "Can't print recipes", issue.subject
279 283 end
280 284
281 285 def test_should_not_recreate_custom_values_objects_on_update
282 286 field = IssueCustomField.find_by_name('Database')
283 287
284 288 issue = Issue.find(1)
285 289 issue.custom_field_values = { field.id => 'PostgreSQL' }
286 290 assert issue.save
287 291 custom_value = issue.custom_value_for(field)
288 292 issue.reload
289 293 issue.custom_field_values = { field.id => 'MySQL' }
290 294 assert issue.save
291 295 issue.reload
292 296 assert_equal custom_value.id, issue.custom_value_for(field).id
293 297 end
294 298
295 299 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
296 300 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'Test', :custom_field_values => {'2' => 'Test'})
297 301 assert !Tracker.find(2).custom_field_ids.include?(2)
298 302
299 303 issue = Issue.find(issue.id)
300 304 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
301 305
302 306 issue = Issue.find(issue.id)
303 307 custom_value = issue.custom_value_for(2)
304 308 assert_not_nil custom_value
305 309 assert_equal 'Test', custom_value.value
306 310 end
307 311
308 312 def test_assigning_tracker_id_should_reload_custom_fields_values
309 313 issue = Issue.new(:project => Project.find(1))
310 314 assert issue.custom_field_values.empty?
311 315 issue.tracker_id = 1
312 316 assert issue.custom_field_values.any?
313 317 end
314 318
315 319 def test_assigning_attributes_should_assign_project_and_tracker_first
316 320 seq = sequence('seq')
317 321 issue = Issue.new
318 322 issue.expects(:project_id=).in_sequence(seq)
319 323 issue.expects(:tracker_id=).in_sequence(seq)
320 324 issue.expects(:subject=).in_sequence(seq)
321 325 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
322 326 end
323 327
324 328 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
325 329 attributes = ActiveSupport::OrderedHash.new
326 330 attributes['custom_field_values'] = { '1' => 'MySQL' }
327 331 attributes['tracker_id'] = '1'
328 332 issue = Issue.new(:project => Project.find(1))
329 333 issue.attributes = attributes
330 334 assert_equal 'MySQL', issue.custom_field_value(1)
331 335 end
332 336
333 337 def test_should_update_issue_with_disabled_tracker
334 338 p = Project.find(1)
335 339 issue = Issue.find(1)
336 340
337 341 p.trackers.delete(issue.tracker)
338 342 assert !p.trackers.include?(issue.tracker)
339 343
340 344 issue.reload
341 345 issue.subject = 'New subject'
342 346 assert issue.save
343 347 end
344 348
345 349 def test_should_not_set_a_disabled_tracker
346 350 p = Project.find(1)
347 351 p.trackers.delete(Tracker.find(2))
348 352
349 353 issue = Issue.find(1)
350 354 issue.tracker_id = 2
351 355 issue.subject = 'New subject'
352 356 assert !issue.save
353 357 assert_not_nil issue.errors[:tracker_id]
354 358 end
355 359
356 360 def test_category_based_assignment
357 361 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
358 362 :status_id => 1, :priority => IssuePriority.all.first,
359 363 :subject => 'Assignment test',
360 364 :description => 'Assignment test', :category_id => 1)
361 365 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
362 366 end
363 367
364 368 def test_new_statuses_allowed_to
365 Workflow.delete_all
369 WorkflowTransition.delete_all
366 370
367 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
368 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
369 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
370 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
371 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
372 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
373 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
374 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
371 375 status = IssueStatus.find(1)
372 376 role = Role.find(1)
373 377 tracker = Tracker.find(1)
374 378 user = User.find(2)
375 379
376 380 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author_id => 1)
377 381 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
378 382
379 383 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
380 384 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
381 385
382 386 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author_id => 1, :assigned_to => user)
383 387 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
384 388
385 389 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
386 390 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
387 391 end
388 392
389 393 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
390 394 admin = User.find(1)
391 395 issue = Issue.find(1)
392 396 assert !admin.member_of?(issue.project)
393 expected_statuses = [issue.status] + Workflow.find_all_by_old_status_id(issue.status_id).map(&:new_status).uniq.sort
397 expected_statuses = [issue.status] + WorkflowTransition.find_all_by_old_status_id(issue.status_id).map(&:new_status).uniq.sort
394 398
395 399 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
396 400 end
397 401
398 402 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
399 403 issue = Issue.find(1).copy
400 404 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
401 405
402 406 issue = Issue.find(2).copy
403 407 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
404 408 end
405 409
406 def test_safe_attributes_should_not_include_disabled_field
410 def test_safe_attributes_names_should_not_include_disabled_field
407 411 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
408 412
409 413 issue = Issue.new(:tracker => tracker)
410 414 assert_include 'tracker_id', issue.safe_attribute_names
411 415 assert_include 'status_id', issue.safe_attribute_names
412 416 assert_include 'subject', issue.safe_attribute_names
413 417 assert_include 'description', issue.safe_attribute_names
414 418 assert_include 'custom_field_values', issue.safe_attribute_names
415 419 assert_include 'custom_fields', issue.safe_attribute_names
416 420 assert_include 'lock_version', issue.safe_attribute_names
417 421
418 422 tracker.core_fields.each do |field|
419 423 assert_include field, issue.safe_attribute_names
420 424 end
421 425
422 426 tracker.disabled_core_fields.each do |field|
423 427 assert_not_include field, issue.safe_attribute_names
424 428 end
425 429 end
426 430
427 431 def test_safe_attributes_should_ignore_disabled_fields
428 432 tracker = Tracker.find(1)
429 433 tracker.core_fields = %w(assigned_to_id due_date)
430 434 tracker.save!
431 435
432 436 issue = Issue.new(:tracker => tracker)
433 437 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
434 438 assert_nil issue.start_date
435 439 assert_equal Date.parse('2012-07-14'), issue.due_date
436 440 end
437 441
438 def test_safe_attributes_should_accept_target_tracker_fields
442 def test_safe_attributes_should_accept_target_tracker_enabled_fields
439 443 source = Tracker.find(1)
440 444 source.core_fields = []
441 445 source.save!
442 446 target = Tracker.find(2)
443 447 target.core_fields = %w(assigned_to_id due_date)
444 448 target.save!
445 449
446 450 issue = Issue.new(:tracker => source)
447 451 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
448 452 assert_equal target, issue.tracker
449 453 assert_equal Date.parse('2012-07-14'), issue.due_date
450 454 end
451 455
456 def test_safe_attributes_should_not_include_readonly_fields
457 WorkflowPermission.delete_all
458 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
459 user = User.find(2)
460
461 issue = Issue.new(:project_id => 1, :tracker_id => 1)
462 assert_equal %w(due_date), issue.read_only_attribute_names(user)
463 assert_not_include 'due_date', issue.safe_attribute_names(user)
464
465 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
466 assert_equal Date.parse('2012-07-14'), issue.start_date
467 assert_nil issue.due_date
468 end
469
470 def test_safe_attributes_should_not_include_readonly_custom_fields
471 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1])
472 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1])
473
474 WorkflowPermission.delete_all
475 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
476 user = User.find(2)
477
478 issue = Issue.new(:project_id => 1, :tracker_id => 1)
479 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
480 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
481
482 issue.send :safe_attributes=, {'custom_field_values' => {cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'}}, user
483 assert_equal 'value1', issue.custom_field_value(cf1)
484 assert_nil issue.custom_field_value(cf2)
485
486 issue.send :safe_attributes=, {'custom_fields' => [{'id' => cf1.id.to_s, 'value' => 'valuea'}, {'id' => cf2.id.to_s, 'value' => 'valueb'}]}, user
487 assert_equal 'valuea', issue.custom_field_value(cf1)
488 assert_nil issue.custom_field_value(cf2)
489 end
490
491 def test_editable_custom_field_values_should_return_non_readonly_custom_values
492 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
493 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
494
495 WorkflowPermission.delete_all
496 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
497 user = User.find(2)
498
499 issue = Issue.new(:project_id => 1, :tracker_id => 1)
500 values = issue.editable_custom_field_values(user)
501 assert values.detect {|value| value.custom_field == cf1}
502 assert_nil values.detect {|value| value.custom_field == cf2}
503
504 issue.tracker_id = 2
505 values = issue.editable_custom_field_values(user)
506 assert values.detect {|value| value.custom_field == cf1}
507 assert values.detect {|value| value.custom_field == cf2}
508 end
509
510 def test_safe_attributes_should_accept_target_tracker_writable_fields
511 WorkflowPermission.delete_all
512 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
513 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'start_date', :rule => 'readonly')
514 user = User.find(2)
515
516 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
517
518 issue.send :safe_attributes=, {'start_date' => '2012-07-12', 'due_date' => '2012-07-14'}, user
519 assert_equal Date.parse('2012-07-12'), issue.start_date
520 assert_nil issue.due_date
521
522 issue.send :safe_attributes=, {'start_date' => '2012-07-15', 'due_date' => '2012-07-16', 'tracker_id' => 2}, user
523 assert_equal Date.parse('2012-07-12'), issue.start_date
524 assert_equal Date.parse('2012-07-16'), issue.due_date
525 end
526
527 def test_safe_attributes_should_accept_target_status_writable_fields
528 WorkflowPermission.delete_all
529 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
530 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'readonly')
531 user = User.find(2)
532
533 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
534
535 issue.send :safe_attributes=, {'start_date' => '2012-07-12', 'due_date' => '2012-07-14'}, user
536 assert_equal Date.parse('2012-07-12'), issue.start_date
537 assert_nil issue.due_date
538
539 issue.send :safe_attributes=, {'start_date' => '2012-07-15', 'due_date' => '2012-07-16', 'status_id' => 2}, user
540 assert_equal Date.parse('2012-07-12'), issue.start_date
541 assert_equal Date.parse('2012-07-16'), issue.due_date
542 end
543
544 def test_required_attributes_should_be_validated
545 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
546
547 WorkflowPermission.delete_all
548 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required')
549 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'category_id', :rule => 'required')
550 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf.id.to_s, :rule => 'required')
551
552 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'start_date', :rule => 'required')
553 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf.id.to_s, :rule => 'required')
554 user = User.find(2)
555
556 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Required fields', :author => user)
557 assert_equal [cf.id.to_s, "category_id", "due_date"], issue.required_attribute_names(user).sort
558 assert !issue.save, "Issue was saved"
559 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"], issue.errors.full_messages.sort
560
561 issue.tracker_id = 2
562 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
563 assert !issue.save, "Issue was saved"
564 assert_equal ["Foo can't be blank", "Start date can't be blank"], issue.errors.full_messages.sort
565
566 issue.start_date = Date.today
567 issue.custom_field_values = {cf.id.to_s => 'bar'}
568 assert issue.save
569 end
570
571 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
572 WorkflowPermission.delete_all
573 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required')
574 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'required')
575 user = User.find(2)
576 member = Member.find(1)
577 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
578
579 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
580
581 member.role_ids = [1, 2]
582 member.save!
583 assert_equal [], issue.required_attribute_names(user.reload)
584
585 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'required')
586 assert_equal %w(due_date), issue.required_attribute_names(user)
587
588 member.role_ids = [1, 2, 3]
589 member.save!
590 assert_equal [], issue.required_attribute_names(user.reload)
591
592 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'readonly')
593 # required + readonly => required
594 assert_equal %w(due_date), issue.required_attribute_names(user)
595 end
596
597 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
598 WorkflowPermission.delete_all
599 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
600 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'readonly')
601 user = User.find(2)
602 member = Member.find(1)
603 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
604
605 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
606
607 member.role_ids = [1, 2]
608 member.save!
609 assert_equal [], issue.read_only_attribute_names(user.reload)
610
611 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'readonly')
612 assert_equal %w(due_date), issue.read_only_attribute_names(user)
613 end
614
452 615 def test_copy
453 616 issue = Issue.new.copy_from(1)
454 617 assert issue.copy?
455 618 assert issue.save
456 619 issue.reload
457 620 orig = Issue.find(1)
458 621 assert_equal orig.subject, issue.subject
459 622 assert_equal orig.tracker, issue.tracker
460 623 assert_equal "125", issue.custom_value_for(2).value
461 624 end
462 625
463 626 def test_copy_should_copy_status
464 627 orig = Issue.find(8)
465 628 assert orig.status != IssueStatus.default
466 629
467 630 issue = Issue.new.copy_from(orig)
468 631 assert issue.save
469 632 issue.reload
470 633 assert_equal orig.status, issue.status
471 634 end
472 635
473 636 def test_should_not_call_after_project_change_on_creation
474 637 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Test', :author_id => 1)
475 638 issue.expects(:after_project_change).never
476 639 issue.save!
477 640 end
478 641
479 642 def test_should_not_call_after_project_change_on_update
480 643 issue = Issue.find(1)
481 644 issue.project = Project.find(1)
482 645 issue.subject = 'No project change'
483 646 issue.expects(:after_project_change).never
484 647 issue.save!
485 648 end
486 649
487 650 def test_should_call_after_project_change_on_project_change
488 651 issue = Issue.find(1)
489 652 issue.project = Project.find(2)
490 653 issue.expects(:after_project_change).once
491 654 issue.save!
492 655 end
493 656
494 657 def test_adding_journal_should_update_timestamp
495 658 issue = Issue.find(1)
496 659 updated_on_was = issue.updated_on
497 660
498 661 issue.init_journal(User.first, "Adding notes")
499 662 assert_difference 'Journal.count' do
500 663 assert issue.save
501 664 end
502 665 issue.reload
503 666
504 667 assert_not_equal updated_on_was, issue.updated_on
505 668 end
506 669
507 670 def test_should_close_duplicates
508 671 # Create 3 issues
509 672 project = Project.find(1)
510 673 issue1 = Issue.generate_for_project!(project)
511 674 issue2 = Issue.generate_for_project!(project)
512 675 issue3 = Issue.generate_for_project!(project)
513 676
514 677 # 2 is a dupe of 1
515 678 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
516 679 # And 3 is a dupe of 2
517 680 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
518 681 # And 3 is a dupe of 1 (circular duplicates)
519 682 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
520 683
521 684 assert issue1.reload.duplicates.include?(issue2)
522 685
523 686 # Closing issue 1
524 687 issue1.init_journal(User.find(:first), "Closing issue1")
525 688 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
526 689 assert issue1.save
527 690 # 2 and 3 should be also closed
528 691 assert issue2.reload.closed?
529 692 assert issue3.reload.closed?
530 693 end
531 694
532 695 def test_should_not_close_duplicated_issue
533 696 project = Project.find(1)
534 697 issue1 = Issue.generate_for_project!(project)
535 698 issue2 = Issue.generate_for_project!(project)
536 699
537 700 # 2 is a dupe of 1
538 701 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
539 702 # 2 is a dup of 1 but 1 is not a duplicate of 2
540 703 assert !issue2.reload.duplicates.include?(issue1)
541 704
542 705 # Closing issue 2
543 706 issue2.init_journal(User.find(:first), "Closing issue2")
544 707 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
545 708 assert issue2.save
546 709 # 1 should not be also closed
547 710 assert !issue1.reload.closed?
548 711 end
549 712
550 713 def test_assignable_versions
551 714 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
552 715 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
553 716 end
554 717
555 718 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
556 719 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
557 720 assert !issue.save
558 721 assert_not_nil issue.errors[:fixed_version_id]
559 722 end
560 723
561 724 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
562 725 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
563 726 assert !issue.save
564 727 assert_not_nil issue.errors[:fixed_version_id]
565 728 end
566 729
567 730 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
568 731 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
569 732 assert issue.save
570 733 end
571 734
572 735 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
573 736 issue = Issue.find(11)
574 737 assert_equal 'closed', issue.fixed_version.status
575 738 issue.subject = 'Subject changed'
576 739 assert issue.save
577 740 end
578 741
579 742 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
580 743 issue = Issue.find(11)
581 744 issue.status_id = 1
582 745 assert !issue.save
583 746 assert_not_nil issue.errors[:base]
584 747 end
585 748
586 749 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
587 750 issue = Issue.find(11)
588 751 issue.status_id = 1
589 752 issue.fixed_version_id = 3
590 753 assert issue.save
591 754 end
592 755
593 756 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
594 757 issue = Issue.find(12)
595 758 assert_equal 'locked', issue.fixed_version.status
596 759 issue.status_id = 1
597 760 assert issue.save
598 761 end
599 762
600 763 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
601 764 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
602 765 end
603 766
604 767 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
605 768 Project.find(2).disable_module! :issue_tracking
606 769 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
607 770 end
608 771
609 772 def test_move_to_another_project_with_same_category
610 773 issue = Issue.find(1)
611 774 issue.project = Project.find(2)
612 775 assert issue.save
613 776 issue.reload
614 777 assert_equal 2, issue.project_id
615 778 # Category changes
616 779 assert_equal 4, issue.category_id
617 780 # Make sure time entries were move to the target project
618 781 assert_equal 2, issue.time_entries.first.project_id
619 782 end
620 783
621 784 def test_move_to_another_project_without_same_category
622 785 issue = Issue.find(2)
623 786 issue.project = Project.find(2)
624 787 assert issue.save
625 788 issue.reload
626 789 assert_equal 2, issue.project_id
627 790 # Category cleared
628 791 assert_nil issue.category_id
629 792 end
630 793
631 794 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
632 795 issue = Issue.find(1)
633 796 issue.update_attribute(:fixed_version_id, 1)
634 797 issue.project = Project.find(2)
635 798 assert issue.save
636 799 issue.reload
637 800 assert_equal 2, issue.project_id
638 801 # Cleared fixed_version
639 802 assert_equal nil, issue.fixed_version
640 803 end
641 804
642 805 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
643 806 issue = Issue.find(1)
644 807 issue.update_attribute(:fixed_version_id, 4)
645 808 issue.project = Project.find(5)
646 809 assert issue.save
647 810 issue.reload
648 811 assert_equal 5, issue.project_id
649 812 # Keep fixed_version
650 813 assert_equal 4, issue.fixed_version_id
651 814 end
652 815
653 816 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
654 817 issue = Issue.find(1)
655 818 issue.update_attribute(:fixed_version_id, 1)
656 819 issue.project = Project.find(5)
657 820 assert issue.save
658 821 issue.reload
659 822 assert_equal 5, issue.project_id
660 823 # Cleared fixed_version
661 824 assert_equal nil, issue.fixed_version
662 825 end
663 826
664 827 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
665 828 issue = Issue.find(1)
666 829 issue.update_attribute(:fixed_version_id, 7)
667 830 issue.project = Project.find(2)
668 831 assert issue.save
669 832 issue.reload
670 833 assert_equal 2, issue.project_id
671 834 # Keep fixed_version
672 835 assert_equal 7, issue.fixed_version_id
673 836 end
674 837
675 838 def test_move_to_another_project_with_disabled_tracker
676 839 issue = Issue.find(1)
677 840 target = Project.find(2)
678 841 target.tracker_ids = [3]
679 842 target.save
680 843 issue.project = target
681 844 assert issue.save
682 845 issue.reload
683 846 assert_equal 2, issue.project_id
684 847 assert_equal 3, issue.tracker_id
685 848 end
686 849
687 850 def test_copy_to_the_same_project
688 851 issue = Issue.find(1)
689 852 copy = issue.copy
690 853 assert_difference 'Issue.count' do
691 854 copy.save!
692 855 end
693 856 assert_kind_of Issue, copy
694 857 assert_equal issue.project, copy.project
695 858 assert_equal "125", copy.custom_value_for(2).value
696 859 end
697 860
698 861 def test_copy_to_another_project_and_tracker
699 862 issue = Issue.find(1)
700 863 copy = issue.copy(:project_id => 3, :tracker_id => 2)
701 864 assert_difference 'Issue.count' do
702 865 copy.save!
703 866 end
704 867 copy.reload
705 868 assert_kind_of Issue, copy
706 869 assert_equal Project.find(3), copy.project
707 870 assert_equal Tracker.find(2), copy.tracker
708 871 # Custom field #2 is not associated with target tracker
709 872 assert_nil copy.custom_value_for(2)
710 873 end
711 874
712 875 context "#copy" do
713 876 setup do
714 877 @issue = Issue.find(1)
715 878 end
716 879
717 880 should "not create a journal" do
718 881 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
719 882 copy.save!
720 883 assert_equal 0, copy.reload.journals.size
721 884 end
722 885
723 886 should "allow assigned_to changes" do
724 887 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
725 888 assert_equal 3, copy.assigned_to_id
726 889 end
727 890
728 891 should "allow status changes" do
729 892 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
730 893 assert_equal 2, copy.status_id
731 894 end
732 895
733 896 should "allow start date changes" do
734 897 date = Date.today
735 898 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
736 899 assert_equal date, copy.start_date
737 900 end
738 901
739 902 should "allow due date changes" do
740 903 date = Date.today
741 904 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
742 905 assert_equal date, copy.due_date
743 906 end
744 907
745 908 should "set current user as author" do
746 909 User.current = User.find(9)
747 910 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
748 911 assert_equal User.current, copy.author
749 912 end
750 913
751 914 should "create a journal with notes" do
752 915 date = Date.today
753 916 notes = "Notes added when copying"
754 917 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
755 918 copy.init_journal(User.current, notes)
756 919 copy.save!
757 920
758 921 assert_equal 1, copy.journals.size
759 922 journal = copy.journals.first
760 923 assert_equal 0, journal.details.size
761 924 assert_equal notes, journal.notes
762 925 end
763 926 end
764 927
765 928 def test_recipients_should_include_previous_assignee
766 929 user = User.find(3)
767 930 user.members.update_all ["mail_notification = ?", false]
768 931 user.update_attribute :mail_notification, 'only_assigned'
769 932
770 933 issue = Issue.find(2)
771 934 issue.assigned_to = nil
772 935 assert_include user.mail, issue.recipients
773 936 issue.save!
774 937 assert !issue.recipients.include?(user.mail)
775 938 end
776 939
777 940 def test_recipients_should_not_include_users_that_cannot_view_the_issue
778 941 issue = Issue.find(12)
779 942 assert issue.recipients.include?(issue.author.mail)
780 943 # copy the issue to a private project
781 944 copy = issue.copy(:project_id => 5, :tracker_id => 2)
782 945 # author is not a member of project anymore
783 946 assert !copy.recipients.include?(copy.author.mail)
784 947 end
785 948
786 949 def test_recipients_should_include_the_assigned_group_members
787 950 group_member = User.generate!
788 951 group = Group.generate!
789 952 group.users << group_member
790 953
791 954 issue = Issue.find(12)
792 955 issue.assigned_to = group
793 956 assert issue.recipients.include?(group_member.mail)
794 957 end
795 958
796 959 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
797 960 user = User.find(3)
798 961 issue = Issue.find(9)
799 962 Watcher.create!(:user => user, :watchable => issue)
800 963 assert issue.watched_by?(user)
801 964 assert !issue.watcher_recipients.include?(user.mail)
802 965 end
803 966
804 967 def test_issue_destroy
805 968 Issue.find(1).destroy
806 969 assert_nil Issue.find_by_id(1)
807 970 assert_nil TimeEntry.find_by_issue_id(1)
808 971 end
809 972
810 973 def test_destroying_a_deleted_issue_should_not_raise_an_error
811 974 issue = Issue.find(1)
812 975 Issue.find(1).destroy
813 976
814 977 assert_nothing_raised do
815 978 assert_no_difference 'Issue.count' do
816 979 issue.destroy
817 980 end
818 981 assert issue.destroyed?
819 982 end
820 983 end
821 984
822 985 def test_destroying_a_stale_issue_should_not_raise_an_error
823 986 issue = Issue.find(1)
824 987 Issue.find(1).update_attribute :subject, "Updated"
825 988
826 989 assert_nothing_raised do
827 990 assert_difference 'Issue.count', -1 do
828 991 issue.destroy
829 992 end
830 993 assert issue.destroyed?
831 994 end
832 995 end
833 996
834 997 def test_blocked
835 998 blocked_issue = Issue.find(9)
836 999 blocking_issue = Issue.find(10)
837 1000
838 1001 assert blocked_issue.blocked?
839 1002 assert !blocking_issue.blocked?
840 1003 end
841 1004
842 1005 def test_blocked_issues_dont_allow_closed_statuses
843 1006 blocked_issue = Issue.find(9)
844 1007
845 1008 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
846 1009 assert !allowed_statuses.empty?
847 1010 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
848 1011 assert closed_statuses.empty?
849 1012 end
850 1013
851 1014 def test_unblocked_issues_allow_closed_statuses
852 1015 blocking_issue = Issue.find(10)
853 1016
854 1017 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
855 1018 assert !allowed_statuses.empty?
856 1019 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
857 1020 assert !closed_statuses.empty?
858 1021 end
859 1022
860 1023 def test_rescheduling_an_issue_should_reschedule_following_issue
861 1024 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
862 1025 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
863 1026 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
864 1027 assert_equal issue1.due_date + 1, issue2.reload.start_date
865 1028
866 1029 issue1.due_date = Date.today + 5
867 1030 issue1.save!
868 1031 assert_equal issue1.due_date + 1, issue2.reload.start_date
869 1032 end
870 1033
871 1034 def test_rescheduling_a_stale_issue_should_not_raise_an_error
872 1035 stale = Issue.find(1)
873 1036 issue = Issue.find(1)
874 1037 issue.subject = "Updated"
875 1038 issue.save!
876 1039
877 1040 date = 10.days.from_now.to_date
878 1041 assert_nothing_raised do
879 1042 stale.reschedule_after(date)
880 1043 end
881 1044 assert_equal date, stale.reload.start_date
882 1045 end
883 1046
884 1047 def test_overdue
885 1048 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
886 1049 assert !Issue.new(:due_date => Date.today).overdue?
887 1050 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
888 1051 assert !Issue.new(:due_date => nil).overdue?
889 1052 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
890 1053 end
891 1054
892 1055 context "#behind_schedule?" do
893 1056 should "be false if the issue has no start_date" do
894 1057 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
895 1058 end
896 1059
897 1060 should "be false if the issue has no end_date" do
898 1061 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
899 1062 end
900 1063
901 1064 should "be false if the issue has more done than it's calendar time" do
902 1065 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
903 1066 end
904 1067
905 1068 should "be true if the issue hasn't been started at all" do
906 1069 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
907 1070 end
908 1071
909 1072 should "be true if the issue has used more calendar time than it's done ratio" do
910 1073 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
911 1074 end
912 1075 end
913 1076
914 1077 context "#assignable_users" do
915 1078 should "be Users" do
916 1079 assert_kind_of User, Issue.find(1).assignable_users.first
917 1080 end
918 1081
919 1082 should "include the issue author" do
920 1083 project = Project.find(1)
921 1084 non_project_member = User.generate!
922 1085 issue = Issue.generate_for_project!(project, :author => non_project_member)
923 1086
924 1087 assert issue.assignable_users.include?(non_project_member)
925 1088 end
926 1089
927 1090 should "include the current assignee" do
928 1091 project = Project.find(1)
929 1092 user = User.generate!
930 1093 issue = Issue.generate_for_project!(project, :assigned_to => user)
931 1094 user.lock!
932 1095
933 1096 assert Issue.find(issue.id).assignable_users.include?(user)
934 1097 end
935 1098
936 1099 should "not show the issue author twice" do
937 1100 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
938 1101 assert_equal 2, assignable_user_ids.length
939 1102
940 1103 assignable_user_ids.each do |user_id|
941 1104 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
942 1105 end
943 1106 end
944 1107
945 1108 context "with issue_group_assignment" do
946 1109 should "include groups" do
947 1110 issue = Issue.new(:project => Project.find(2))
948 1111
949 1112 with_settings :issue_group_assignment => '1' do
950 1113 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
951 1114 assert issue.assignable_users.include?(Group.find(11))
952 1115 end
953 1116 end
954 1117 end
955 1118
956 1119 context "without issue_group_assignment" do
957 1120 should "not include groups" do
958 1121 issue = Issue.new(:project => Project.find(2))
959 1122
960 1123 with_settings :issue_group_assignment => '0' do
961 1124 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
962 1125 assert !issue.assignable_users.include?(Group.find(11))
963 1126 end
964 1127 end
965 1128 end
966 1129 end
967 1130
968 1131 def test_create_should_send_email_notification
969 1132 ActionMailer::Base.deliveries.clear
970 1133 issue = Issue.new(:project_id => 1, :tracker_id => 1,
971 1134 :author_id => 3, :status_id => 1,
972 1135 :priority => IssuePriority.all.first,
973 1136 :subject => 'test_create', :estimated_hours => '1:30')
974 1137
975 1138 assert issue.save
976 1139 assert_equal 1, ActionMailer::Base.deliveries.size
977 1140 end
978 1141
979 1142 def test_stale_issue_should_not_send_email_notification
980 1143 ActionMailer::Base.deliveries.clear
981 1144 issue = Issue.find(1)
982 1145 stale = Issue.find(1)
983 1146
984 1147 issue.init_journal(User.find(1))
985 1148 issue.subject = 'Subjet update'
986 1149 assert issue.save
987 1150 assert_equal 1, ActionMailer::Base.deliveries.size
988 1151 ActionMailer::Base.deliveries.clear
989 1152
990 1153 stale.init_journal(User.find(1))
991 1154 stale.subject = 'Another subjet update'
992 1155 assert_raise ActiveRecord::StaleObjectError do
993 1156 stale.save
994 1157 end
995 1158 assert ActionMailer::Base.deliveries.empty?
996 1159 end
997 1160
998 1161 def test_journalized_description
999 1162 IssueCustomField.delete_all
1000 1163
1001 1164 i = Issue.first
1002 1165 old_description = i.description
1003 1166 new_description = "This is the new description"
1004 1167
1005 1168 i.init_journal(User.find(2))
1006 1169 i.description = new_description
1007 1170 assert_difference 'Journal.count', 1 do
1008 1171 assert_difference 'JournalDetail.count', 1 do
1009 1172 i.save!
1010 1173 end
1011 1174 end
1012 1175
1013 1176 detail = JournalDetail.first(:order => 'id DESC')
1014 1177 assert_equal i, detail.journal.journalized
1015 1178 assert_equal 'attr', detail.property
1016 1179 assert_equal 'description', detail.prop_key
1017 1180 assert_equal old_description, detail.old_value
1018 1181 assert_equal new_description, detail.value
1019 1182 end
1020 1183
1021 1184 def test_blank_descriptions_should_not_be_journalized
1022 1185 IssueCustomField.delete_all
1023 1186 Issue.update_all("description = NULL", "id=1")
1024 1187
1025 1188 i = Issue.find(1)
1026 1189 i.init_journal(User.find(2))
1027 1190 i.subject = "blank description"
1028 1191 i.description = "\r\n"
1029 1192
1030 1193 assert_difference 'Journal.count', 1 do
1031 1194 assert_difference 'JournalDetail.count', 1 do
1032 1195 i.save!
1033 1196 end
1034 1197 end
1035 1198 end
1036 1199
1037 1200 def test_journalized_multi_custom_field
1038 1201 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
1039 1202 :tracker_ids => [1], :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
1040 1203
1041 1204 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'Test', :author_id => 1)
1042 1205
1043 1206 assert_difference 'Journal.count' do
1044 1207 assert_difference 'JournalDetail.count' do
1045 1208 issue.init_journal(User.first)
1046 1209 issue.custom_field_values = {field.id => ['value1']}
1047 1210 issue.save!
1048 1211 end
1049 1212 assert_difference 'JournalDetail.count' do
1050 1213 issue.init_journal(User.first)
1051 1214 issue.custom_field_values = {field.id => ['value1', 'value2']}
1052 1215 issue.save!
1053 1216 end
1054 1217 assert_difference 'JournalDetail.count', 2 do
1055 1218 issue.init_journal(User.first)
1056 1219 issue.custom_field_values = {field.id => ['value3', 'value2']}
1057 1220 issue.save!
1058 1221 end
1059 1222 assert_difference 'JournalDetail.count', 2 do
1060 1223 issue.init_journal(User.first)
1061 1224 issue.custom_field_values = {field.id => nil}
1062 1225 issue.save!
1063 1226 end
1064 1227 end
1065 1228 end
1066 1229
1067 1230 def test_description_eol_should_be_normalized
1068 1231 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1069 1232 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1070 1233 end
1071 1234
1072 1235 def test_saving_twice_should_not_duplicate_journal_details
1073 1236 i = Issue.find(:first)
1074 1237 i.init_journal(User.find(2), 'Some notes')
1075 1238 # initial changes
1076 1239 i.subject = 'New subject'
1077 1240 i.done_ratio = i.done_ratio + 10
1078 1241 assert_difference 'Journal.count' do
1079 1242 assert i.save
1080 1243 end
1081 1244 # 1 more change
1082 1245 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
1083 1246 assert_no_difference 'Journal.count' do
1084 1247 assert_difference 'JournalDetail.count', 1 do
1085 1248 i.save
1086 1249 end
1087 1250 end
1088 1251 # no more change
1089 1252 assert_no_difference 'Journal.count' do
1090 1253 assert_no_difference 'JournalDetail.count' do
1091 1254 i.save
1092 1255 end
1093 1256 end
1094 1257 end
1095 1258
1096 1259 def test_all_dependent_issues
1097 1260 IssueRelation.delete_all
1098 1261 assert IssueRelation.create!(:issue_from => Issue.find(1),
1099 1262 :issue_to => Issue.find(2),
1100 1263 :relation_type => IssueRelation::TYPE_PRECEDES)
1101 1264 assert IssueRelation.create!(:issue_from => Issue.find(2),
1102 1265 :issue_to => Issue.find(3),
1103 1266 :relation_type => IssueRelation::TYPE_PRECEDES)
1104 1267 assert IssueRelation.create!(:issue_from => Issue.find(3),
1105 1268 :issue_to => Issue.find(8),
1106 1269 :relation_type => IssueRelation::TYPE_PRECEDES)
1107 1270
1108 1271 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1109 1272 end
1110 1273
1111 1274 def test_all_dependent_issues_with_persistent_circular_dependency
1112 1275 IssueRelation.delete_all
1113 1276 assert IssueRelation.create!(:issue_from => Issue.find(1),
1114 1277 :issue_to => Issue.find(2),
1115 1278 :relation_type => IssueRelation::TYPE_PRECEDES)
1116 1279 assert IssueRelation.create!(:issue_from => Issue.find(2),
1117 1280 :issue_to => Issue.find(3),
1118 1281 :relation_type => IssueRelation::TYPE_PRECEDES)
1119 1282
1120 1283 r = IssueRelation.create!(:issue_from => Issue.find(3),
1121 1284 :issue_to => Issue.find(7),
1122 1285 :relation_type => IssueRelation::TYPE_PRECEDES)
1123 1286 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1124 1287
1125 1288 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1126 1289 end
1127 1290
1128 1291 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1129 1292 IssueRelation.delete_all
1130 1293 assert IssueRelation.create!(:issue_from => Issue.find(1),
1131 1294 :issue_to => Issue.find(2),
1132 1295 :relation_type => IssueRelation::TYPE_RELATES)
1133 1296 assert IssueRelation.create!(:issue_from => Issue.find(2),
1134 1297 :issue_to => Issue.find(3),
1135 1298 :relation_type => IssueRelation::TYPE_RELATES)
1136 1299 assert IssueRelation.create!(:issue_from => Issue.find(3),
1137 1300 :issue_to => Issue.find(8),
1138 1301 :relation_type => IssueRelation::TYPE_RELATES)
1139 1302
1140 1303 r = IssueRelation.create!(:issue_from => Issue.find(8),
1141 1304 :issue_to => Issue.find(7),
1142 1305 :relation_type => IssueRelation::TYPE_RELATES)
1143 1306 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1144 1307
1145 1308 r = IssueRelation.create!(:issue_from => Issue.find(3),
1146 1309 :issue_to => Issue.find(7),
1147 1310 :relation_type => IssueRelation::TYPE_RELATES)
1148 1311 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1149 1312
1150 1313 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1151 1314 end
1152 1315
1153 1316 context "#done_ratio" do
1154 1317 setup do
1155 1318 @issue = Issue.find(1)
1156 1319 @issue_status = IssueStatus.find(1)
1157 1320 @issue_status.update_attribute(:default_done_ratio, 50)
1158 1321 @issue2 = Issue.find(2)
1159 1322 @issue_status2 = IssueStatus.find(2)
1160 1323 @issue_status2.update_attribute(:default_done_ratio, 0)
1161 1324 end
1162 1325
1163 1326 teardown do
1164 1327 Setting.issue_done_ratio = 'issue_field'
1165 1328 end
1166 1329
1167 1330 context "with Setting.issue_done_ratio using the issue_field" do
1168 1331 setup do
1169 1332 Setting.issue_done_ratio = 'issue_field'
1170 1333 end
1171 1334
1172 1335 should "read the issue's field" do
1173 1336 assert_equal 0, @issue.done_ratio
1174 1337 assert_equal 30, @issue2.done_ratio
1175 1338 end
1176 1339 end
1177 1340
1178 1341 context "with Setting.issue_done_ratio using the issue_status" do
1179 1342 setup do
1180 1343 Setting.issue_done_ratio = 'issue_status'
1181 1344 end
1182 1345
1183 1346 should "read the Issue Status's default done ratio" do
1184 1347 assert_equal 50, @issue.done_ratio
1185 1348 assert_equal 0, @issue2.done_ratio
1186 1349 end
1187 1350 end
1188 1351 end
1189 1352
1190 1353 context "#update_done_ratio_from_issue_status" do
1191 1354 setup do
1192 1355 @issue = Issue.find(1)
1193 1356 @issue_status = IssueStatus.find(1)
1194 1357 @issue_status.update_attribute(:default_done_ratio, 50)
1195 1358 @issue2 = Issue.find(2)
1196 1359 @issue_status2 = IssueStatus.find(2)
1197 1360 @issue_status2.update_attribute(:default_done_ratio, 0)
1198 1361 end
1199 1362
1200 1363 context "with Setting.issue_done_ratio using the issue_field" do
1201 1364 setup do
1202 1365 Setting.issue_done_ratio = 'issue_field'
1203 1366 end
1204 1367
1205 1368 should "not change the issue" do
1206 1369 @issue.update_done_ratio_from_issue_status
1207 1370 @issue2.update_done_ratio_from_issue_status
1208 1371
1209 1372 assert_equal 0, @issue.read_attribute(:done_ratio)
1210 1373 assert_equal 30, @issue2.read_attribute(:done_ratio)
1211 1374 end
1212 1375 end
1213 1376
1214 1377 context "with Setting.issue_done_ratio using the issue_status" do
1215 1378 setup do
1216 1379 Setting.issue_done_ratio = 'issue_status'
1217 1380 end
1218 1381
1219 1382 should "change the issue's done ratio" do
1220 1383 @issue.update_done_ratio_from_issue_status
1221 1384 @issue2.update_done_ratio_from_issue_status
1222 1385
1223 1386 assert_equal 50, @issue.read_attribute(:done_ratio)
1224 1387 assert_equal 0, @issue2.read_attribute(:done_ratio)
1225 1388 end
1226 1389 end
1227 1390 end
1228 1391
1229 1392 test "#by_tracker" do
1230 1393 User.current = User.anonymous
1231 1394 groups = Issue.by_tracker(Project.find(1))
1232 1395 assert_equal 3, groups.size
1233 1396 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1234 1397 end
1235 1398
1236 1399 test "#by_version" do
1237 1400 User.current = User.anonymous
1238 1401 groups = Issue.by_version(Project.find(1))
1239 1402 assert_equal 3, groups.size
1240 1403 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1241 1404 end
1242 1405
1243 1406 test "#by_priority" do
1244 1407 User.current = User.anonymous
1245 1408 groups = Issue.by_priority(Project.find(1))
1246 1409 assert_equal 4, groups.size
1247 1410 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1248 1411 end
1249 1412
1250 1413 test "#by_category" do
1251 1414 User.current = User.anonymous
1252 1415 groups = Issue.by_category(Project.find(1))
1253 1416 assert_equal 2, groups.size
1254 1417 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1255 1418 end
1256 1419
1257 1420 test "#by_assigned_to" do
1258 1421 User.current = User.anonymous
1259 1422 groups = Issue.by_assigned_to(Project.find(1))
1260 1423 assert_equal 2, groups.size
1261 1424 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1262 1425 end
1263 1426
1264 1427 test "#by_author" do
1265 1428 User.current = User.anonymous
1266 1429 groups = Issue.by_author(Project.find(1))
1267 1430 assert_equal 4, groups.size
1268 1431 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1269 1432 end
1270 1433
1271 1434 test "#by_subproject" do
1272 1435 User.current = User.anonymous
1273 1436 groups = Issue.by_subproject(Project.find(1))
1274 1437 # Private descendant not visible
1275 1438 assert_equal 1, groups.size
1276 1439 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1277 1440 end
1278 1441
1279 1442 def test_recently_updated_with_limit_scopes
1280 1443 #should return the last updated issue
1281 1444 assert_equal 1, Issue.recently_updated.with_limit(1).length
1282 1445 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1283 1446 end
1284 1447
1285 1448 def test_on_active_projects_scope
1286 1449 assert Project.find(2).archive
1287 1450
1288 1451 before = Issue.on_active_project.length
1289 1452 # test inclusion to results
1290 1453 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1291 1454 assert_equal before + 1, Issue.on_active_project.length
1292 1455
1293 1456 # Move to an archived project
1294 1457 issue.project = Project.find(2)
1295 1458 assert issue.save
1296 1459 assert_equal before, Issue.on_active_project.length
1297 1460 end
1298 1461
1299 1462 context "Issue#recipients" do
1300 1463 setup do
1301 1464 @project = Project.find(1)
1302 1465 @author = User.generate!
1303 1466 @assignee = User.generate!
1304 1467 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1305 1468 end
1306 1469
1307 1470 should "include project recipients" do
1308 1471 assert @project.recipients.present?
1309 1472 @project.recipients.each do |project_recipient|
1310 1473 assert @issue.recipients.include?(project_recipient)
1311 1474 end
1312 1475 end
1313 1476
1314 1477 should "include the author if the author is active" do
1315 1478 assert @issue.author, "No author set for Issue"
1316 1479 assert @issue.recipients.include?(@issue.author.mail)
1317 1480 end
1318 1481
1319 1482 should "include the assigned to user if the assigned to user is active" do
1320 1483 assert @issue.assigned_to, "No assigned_to set for Issue"
1321 1484 assert @issue.recipients.include?(@issue.assigned_to.mail)
1322 1485 end
1323 1486
1324 1487 should "not include users who opt out of all email" do
1325 1488 @author.update_attribute(:mail_notification, :none)
1326 1489
1327 1490 assert !@issue.recipients.include?(@issue.author.mail)
1328 1491 end
1329 1492
1330 1493 should "not include the issue author if they are only notified of assigned issues" do
1331 1494 @author.update_attribute(:mail_notification, :only_assigned)
1332 1495
1333 1496 assert !@issue.recipients.include?(@issue.author.mail)
1334 1497 end
1335 1498
1336 1499 should "not include the assigned user if they are only notified of owned issues" do
1337 1500 @assignee.update_attribute(:mail_notification, :only_owner)
1338 1501
1339 1502 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1340 1503 end
1341 1504 end
1342 1505
1343 1506 def test_last_journal_id_with_journals_should_return_the_journal_id
1344 1507 assert_equal 2, Issue.find(1).last_journal_id
1345 1508 end
1346 1509
1347 1510 def test_last_journal_id_without_journals_should_return_nil
1348 1511 assert_nil Issue.find(3).last_journal_id
1349 1512 end
1350 1513 end
@@ -1,133 +1,133
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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
22 22
23 23 def test_sorted_scope
24 24 assert_equal Role.all.sort, Role.sorted.all
25 25 end
26 26
27 27 def test_givable_scope
28 28 assert_equal Role.all.reject(&:builtin?).sort, Role.givable.all
29 29 end
30 30
31 31 def test_builtin_scope
32 32 assert_equal Role.all.select(&:builtin?).sort, Role.builtin(true).all.sort
33 33 assert_equal Role.all.reject(&:builtin?).sort, Role.builtin(false).all.sort
34 34 end
35 35
36 36 def test_copy_workflows
37 37 source = Role.find(1)
38 assert_equal 90, source.workflows.size
38 assert_equal 90, source.workflow_rules.size
39 39
40 40 target = Role.new(:name => 'Target')
41 41 assert target.save
42 target.workflows.copy(source)
42 target.workflow_rules.copy(source)
43 43 target.reload
44 assert_equal 90, target.workflows.size
44 assert_equal 90, target.workflow_rules.size
45 45 end
46 46
47 47 def test_permissions_should_be_unserialized_with_its_coder
48 48 Role::PermissionsAttributeCoder.expects(:load).once
49 49 Role.find(1).permissions
50 50 end
51 51
52 52 def test_add_permission
53 53 role = Role.find(1)
54 54 size = role.permissions.size
55 55 role.add_permission!("apermission", "anotherpermission")
56 56 role.reload
57 57 assert role.permissions.include?(:anotherpermission)
58 58 assert_equal size + 2, role.permissions.size
59 59 end
60 60
61 61 def test_remove_permission
62 62 role = Role.find(1)
63 63 size = role.permissions.size
64 64 perm = role.permissions[0..1]
65 65 role.remove_permission!(*perm)
66 66 role.reload
67 67 assert ! role.permissions.include?(perm[0])
68 68 assert_equal size - 2, role.permissions.size
69 69 end
70 70
71 71 def test_name
72 72 I18n.locale = 'fr'
73 73 assert_equal 'Manager', Role.find(1).name
74 74 assert_equal 'Anonyme', Role.anonymous.name
75 75 assert_equal 'Non membre', Role.non_member.name
76 76 end
77 77
78 78 def test_find_all_givable
79 79 assert_equal Role.all.reject(&:builtin?).sort, Role.find_all_givable
80 80 end
81 81
82 82 context "#anonymous" do
83 83 should "return the anonymous role" do
84 84 role = Role.anonymous
85 85 assert role.builtin?
86 86 assert_equal Role::BUILTIN_ANONYMOUS, role.builtin
87 87 end
88 88
89 89 context "with a missing anonymous role" do
90 90 setup do
91 91 Role.delete_all("builtin = #{Role::BUILTIN_ANONYMOUS}")
92 92 end
93 93
94 94 should "create a new anonymous role" do
95 95 assert_difference('Role.count') do
96 96 Role.anonymous
97 97 end
98 98 end
99 99
100 100 should "return the anonymous role" do
101 101 role = Role.anonymous
102 102 assert role.builtin?
103 103 assert_equal Role::BUILTIN_ANONYMOUS, role.builtin
104 104 end
105 105 end
106 106 end
107 107
108 108 context "#non_member" do
109 109 should "return the non-member role" do
110 110 role = Role.non_member
111 111 assert role.builtin?
112 112 assert_equal Role::BUILTIN_NON_MEMBER, role.builtin
113 113 end
114 114
115 115 context "with a missing non-member role" do
116 116 setup do
117 117 Role.delete_all("builtin = #{Role::BUILTIN_NON_MEMBER}")
118 118 end
119 119
120 120 should "create a new non-member role" do
121 121 assert_difference('Role.count') do
122 122 Role.non_member
123 123 end
124 124 end
125 125
126 126 should "return the non-member role" do
127 127 role = Role.non_member
128 128 assert role.builtin?
129 129 assert_equal Role::BUILTIN_NON_MEMBER, role.builtin
130 130 end
131 131 end
132 132 end
133 133 end
@@ -1,113 +1,113
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.all
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.workflows.size
33 assert_equal 89, source.workflow_rules.size
34 34
35 35 target = Tracker.new(:name => 'Target')
36 36 assert target.save
37 target.workflows.copy(source)
37 target.workflow_rules.copy(source)
38 38 target.reload
39 assert_equal 89, target.workflows.size
39 assert_equal 89, target.workflow_rules.size
40 40 end
41 41
42 42 def test_issue_statuses
43 43 tracker = Tracker.find(1)
44 Workflow.delete_all
45 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
46 Workflow.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
44 WorkflowTransition.delete_all
45 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
46 WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
47 47
48 48 assert_kind_of Array, tracker.issue_statuses
49 49 assert_kind_of IssueStatus, tracker.issue_statuses.first
50 50 assert_equal [2, 3, 5], Tracker.find(1).issue_statuses.collect(&:id)
51 51 end
52 52
53 53 def test_issue_statuses_empty
54 Workflow.delete_all("tracker_id = 1")
54 WorkflowTransition.delete_all("tracker_id = 1")
55 55 assert_equal [], Tracker.find(1).issue_statuses
56 56 end
57 57
58 58 def test_issue_statuses_should_be_empty_for_new_record
59 59 assert_equal [], Tracker.new.issue_statuses
60 60 end
61 61
62 62 def test_core_fields_should_be_enabled_by_default
63 63 tracker = Tracker.new
64 64 assert_equal Tracker::CORE_FIELDS, tracker.core_fields
65 65 assert_equal [], tracker.disabled_core_fields
66 66 end
67 67
68 68 def test_core_fields
69 69 tracker = Tracker.new
70 70 tracker.core_fields = %w(assigned_to_id due_date)
71 71
72 72 assert_equal %w(assigned_to_id due_date), tracker.core_fields
73 73 assert_equal Tracker::CORE_FIELDS - %w(assigned_to_id due_date), tracker.disabled_core_fields
74 74 end
75 75
76 76 def test_core_fields_should_return_fields_enabled_for_any_tracker
77 77 trackers = []
78 78 trackers << Tracker.new(:core_fields => %w(assigned_to_id due_date))
79 79 trackers << Tracker.new(:core_fields => %w(assigned_to_id done_ratio))
80 80 trackers << Tracker.new(:core_fields => [])
81 81
82 82 assert_equal %w(assigned_to_id due_date done_ratio), Tracker.core_fields(trackers)
83 83 assert_equal Tracker::CORE_FIELDS - %w(assigned_to_id due_date done_ratio), Tracker.disabled_core_fields(trackers)
84 84 end
85 85
86 86 def test_sort_should_sort_by_position
87 87 a = Tracker.new(:name => 'Tracker A', :position => 2)
88 88 b = Tracker.new(:name => 'Tracker B', :position => 1)
89 89
90 90 assert_equal [b, a], [a, b].sort
91 91 end
92 92
93 93 def test_destroying_a_tracker_without_issues_should_not_raise_an_error
94 94 tracker = Tracker.find(1)
95 95 Issue.delete_all :tracker_id => tracker.id
96 96
97 97 assert_difference 'Tracker.count', -1 do
98 98 assert_nothing_raised do
99 99 tracker.destroy
100 100 end
101 101 end
102 102 end
103 103
104 104 def test_destroying_a_tracker_with_issues_should_raise_an_error
105 105 tracker = Tracker.find(1)
106 106
107 107 assert_no_difference 'Tracker.count' do
108 108 assert_raise Exception do
109 109 tracker.destroy
110 110 end
111 111 end
112 112 end
113 113 end
@@ -1,37 +1,65
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 WorkflowTest < ActiveSupport::TestCase
21 21 fixtures :roles, :trackers, :issue_statuses
22 22
23 23 def test_copy
24 Workflow.delete_all
25 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 2)
26 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 3, :assignee => true)
27 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 4, :author => true)
24 WorkflowTransition.delete_all
25 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 2)
26 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 3, :assignee => true)
27 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 4, :author => true)
28 28
29 assert_difference 'Workflow.count', 3 do
30 Workflow.copy(Tracker.find(2), Role.find(1), Tracker.find(3), Role.find(2))
29 assert_difference 'WorkflowTransition.count', 3 do
30 WorkflowTransition.copy(Tracker.find(2), Role.find(1), Tracker.find(3), Role.find(2))
31 31 end
32 32
33 assert Workflow.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false})
34 assert Workflow.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 3, :author => false, :assignee => true})
35 assert Workflow.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 4, :author => true, :assignee => false})
33 assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false})
34 assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 3, :author => false, :assignee => true})
35 assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 4, :author => true, :assignee => false})
36 end
37
38 def test_workflow_permission_should_validate_rule
39 wp = WorkflowPermission.new(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :field_name => 'due_date')
40 assert !wp.save
41
42 wp.rule = 'foo'
43 assert !wp.save
44
45 wp.rule = 'required'
46 assert wp.save
47
48 wp.rule = 'readonly'
49 assert wp.save
50 end
51
52 def test_workflow_permission_should_validate_field_name
53 wp = WorkflowPermission.new(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :rule => 'required')
54 assert !wp.save
55
56 wp.field_name = 'foo'
57 assert !wp.save
58
59 wp.field_name = 'due_date'
60 assert wp.save
61
62 wp.field_name = '1'
63 assert wp.save
36 64 end
37 65 end
General Comments 0
You need to be logged in to leave comments. Login now