##// END OF EJS Templates
Added Issue#attributes_editable?...
Jean-Philippe Lang -
r13614:37aa01674039
parent child
Show More
@@ -1,519 +1,517
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => [:new, :create]
19 menu_item :new_issue, :only => [:new, :create]
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update]
22 before_filter :find_issue, :only => [:show, :edit, :update]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
24 before_filter :find_project, :only => [:new, :create, :update_form]
24 before_filter :find_project, :only => [:new, :create, :update_form]
25 before_filter :authorize, :except => [:index]
25 before_filter :authorize, :except => [:index]
26 before_filter :find_optional_project, :only => [:index]
26 before_filter :find_optional_project, :only => [:index]
27 before_filter :build_new_issue_from_params, :only => [:new, :create, :update_form]
27 before_filter :build_new_issue_from_params, :only => [:new, :create, :update_form]
28 accept_rss_auth :index, :show
28 accept_rss_auth :index, :show
29 accept_api_auth :index, :show, :create, :update, :destroy
29 accept_api_auth :index, :show, :create, :update, :destroy
30
30
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32
32
33 helper :journals
33 helper :journals
34 helper :projects
34 helper :projects
35 include ProjectsHelper
35 include ProjectsHelper
36 helper :custom_fields
36 helper :custom_fields
37 include CustomFieldsHelper
37 include CustomFieldsHelper
38 helper :issue_relations
38 helper :issue_relations
39 include IssueRelationsHelper
39 include IssueRelationsHelper
40 helper :watchers
40 helper :watchers
41 include WatchersHelper
41 include WatchersHelper
42 helper :attachments
42 helper :attachments
43 include AttachmentsHelper
43 include AttachmentsHelper
44 helper :queries
44 helper :queries
45 include QueriesHelper
45 include QueriesHelper
46 helper :repositories
46 helper :repositories
47 include RepositoriesHelper
47 include RepositoriesHelper
48 helper :sort
48 helper :sort
49 include SortHelper
49 include SortHelper
50 include IssuesHelper
50 include IssuesHelper
51 helper :timelog
51 helper :timelog
52
52
53 def index
53 def index
54 retrieve_query
54 retrieve_query
55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
56 sort_update(@query.sortable_columns)
56 sort_update(@query.sortable_columns)
57 @query.sort_criteria = sort_criteria.to_a
57 @query.sort_criteria = sort_criteria.to_a
58
58
59 if @query.valid?
59 if @query.valid?
60 case params[:format]
60 case params[:format]
61 when 'csv', 'pdf'
61 when 'csv', 'pdf'
62 @limit = Setting.issues_export_limit.to_i
62 @limit = Setting.issues_export_limit.to_i
63 if params[:columns] == 'all'
63 if params[:columns] == 'all'
64 @query.column_names = @query.available_inline_columns.map(&:name)
64 @query.column_names = @query.available_inline_columns.map(&:name)
65 end
65 end
66 when 'atom'
66 when 'atom'
67 @limit = Setting.feeds_limit.to_i
67 @limit = Setting.feeds_limit.to_i
68 when 'xml', 'json'
68 when 'xml', 'json'
69 @offset, @limit = api_offset_and_limit
69 @offset, @limit = api_offset_and_limit
70 @query.column_names = %w(author)
70 @query.column_names = %w(author)
71 else
71 else
72 @limit = per_page_option
72 @limit = per_page_option
73 end
73 end
74
74
75 @issue_count = @query.issue_count
75 @issue_count = @query.issue_count
76 @issue_pages = Paginator.new @issue_count, @limit, params['page']
76 @issue_pages = Paginator.new @issue_count, @limit, params['page']
77 @offset ||= @issue_pages.offset
77 @offset ||= @issue_pages.offset
78 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
78 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
79 :order => sort_clause,
79 :order => sort_clause,
80 :offset => @offset,
80 :offset => @offset,
81 :limit => @limit)
81 :limit => @limit)
82 @issue_count_by_group = @query.issue_count_by_group
82 @issue_count_by_group = @query.issue_count_by_group
83
83
84 respond_to do |format|
84 respond_to do |format|
85 format.html { render :template => 'issues/index', :layout => !request.xhr? }
85 format.html { render :template => 'issues/index', :layout => !request.xhr? }
86 format.api {
86 format.api {
87 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
87 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
88 }
88 }
89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
90 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
90 format.csv { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
91 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
91 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
92 end
92 end
93 else
93 else
94 respond_to do |format|
94 respond_to do |format|
95 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
95 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
96 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
96 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
97 format.api { render_validation_errors(@query) }
97 format.api { render_validation_errors(@query) }
98 end
98 end
99 end
99 end
100 rescue ActiveRecord::RecordNotFound
100 rescue ActiveRecord::RecordNotFound
101 render_404
101 render_404
102 end
102 end
103
103
104 def show
104 def show
105 @journals = @issue.journals.includes(:user, :details).
105 @journals = @issue.journals.includes(:user, :details).
106 references(:user, :details).
106 references(:user, :details).
107 reorder("#{Journal.table_name}.id ASC").to_a
107 reorder("#{Journal.table_name}.id ASC").to_a
108 @journals.each_with_index {|j,i| j.indice = i+1}
108 @journals.each_with_index {|j,i| j.indice = i+1}
109 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
109 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
110 Journal.preload_journals_details_custom_fields(@journals)
110 Journal.preload_journals_details_custom_fields(@journals)
111 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
111 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
113
113
114 @changesets = @issue.changesets.visible.to_a
114 @changesets = @issue.changesets.visible.to_a
115 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
115 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
116
116
117 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
117 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
118 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
118 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
119 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
120 @priorities = IssuePriority.active
119 @priorities = IssuePriority.active
121 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
120 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
122 @relation = IssueRelation.new
121 @relation = IssueRelation.new
123
122
124 respond_to do |format|
123 respond_to do |format|
125 format.html {
124 format.html {
126 retrieve_previous_and_next_issue_ids
125 retrieve_previous_and_next_issue_ids
127 render :template => 'issues/show'
126 render :template => 'issues/show'
128 }
127 }
129 format.api
128 format.api
130 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
129 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
131 format.pdf {
130 format.pdf {
132 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
131 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
133 }
132 }
134 end
133 end
135 end
134 end
136
135
137 # Add a new issue
136 # Add a new issue
138 # The new issue will be created from an existing one if copy_from parameter is given
137 # The new issue will be created from an existing one if copy_from parameter is given
139 def new
138 def new
140 respond_to do |format|
139 respond_to do |format|
141 format.html { render :action => 'new', :layout => !request.xhr? }
140 format.html { render :action => 'new', :layout => !request.xhr? }
142 end
141 end
143 end
142 end
144
143
145 def create
144 def create
146 unless User.current.allowed_to?(:add_issues, @issue.project)
145 unless User.current.allowed_to?(:add_issues, @issue.project)
147 raise ::Unauthorized
146 raise ::Unauthorized
148 end
147 end
149 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
148 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
150 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
149 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
151 if @issue.save
150 if @issue.save
152 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
151 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
153 respond_to do |format|
152 respond_to do |format|
154 format.html {
153 format.html {
155 render_attachment_warning_if_needed(@issue)
154 render_attachment_warning_if_needed(@issue)
156 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
155 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
157 if params[:continue]
156 if params[:continue]
158 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
157 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
159 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
158 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
160 else
159 else
161 redirect_to issue_path(@issue)
160 redirect_to issue_path(@issue)
162 end
161 end
163 }
162 }
164 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
163 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
165 end
164 end
166 return
165 return
167 else
166 else
168 respond_to do |format|
167 respond_to do |format|
169 format.html { render :action => 'new' }
168 format.html { render :action => 'new' }
170 format.api { render_validation_errors(@issue) }
169 format.api { render_validation_errors(@issue) }
171 end
170 end
172 end
171 end
173 end
172 end
174
173
175 def edit
174 def edit
176 return unless update_issue_from_params
175 return unless update_issue_from_params
177
176
178 respond_to do |format|
177 respond_to do |format|
179 format.html { }
178 format.html { }
180 format.xml { }
179 format.xml { }
181 end
180 end
182 end
181 end
183
182
184 def update
183 def update
185 return unless update_issue_from_params
184 return unless update_issue_from_params
186 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
185 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
187 saved = false
186 saved = false
188 begin
187 begin
189 saved = save_issue_with_child_records
188 saved = save_issue_with_child_records
190 rescue ActiveRecord::StaleObjectError
189 rescue ActiveRecord::StaleObjectError
191 @conflict = true
190 @conflict = true
192 if params[:last_journal_id]
191 if params[:last_journal_id]
193 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
192 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
194 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
193 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
195 end
194 end
196 end
195 end
197
196
198 if saved
197 if saved
199 render_attachment_warning_if_needed(@issue)
198 render_attachment_warning_if_needed(@issue)
200 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
199 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
201
200
202 respond_to do |format|
201 respond_to do |format|
203 format.html { redirect_back_or_default issue_path(@issue) }
202 format.html { redirect_back_or_default issue_path(@issue) }
204 format.api { render_api_ok }
203 format.api { render_api_ok }
205 end
204 end
206 else
205 else
207 respond_to do |format|
206 respond_to do |format|
208 format.html { render :action => 'edit' }
207 format.html { render :action => 'edit' }
209 format.api { render_validation_errors(@issue) }
208 format.api { render_validation_errors(@issue) }
210 end
209 end
211 end
210 end
212 end
211 end
213
212
214 # Updates the issue form when changing the project, status or tracker
213 # Updates the issue form when changing the project, status or tracker
215 # on issue creation/update
214 # on issue creation/update
216 def update_form
215 def update_form
217 end
216 end
218
217
219 # Bulk edit/copy a set of issues
218 # Bulk edit/copy a set of issues
220 def bulk_edit
219 def bulk_edit
221 @issues.sort!
220 @issues.sort!
222 @copy = params[:copy].present?
221 @copy = params[:copy].present?
223 @notes = params[:notes]
222 @notes = params[:notes]
224
223
225 if @copy
224 if @copy
226 unless User.current.allowed_to?(:copy_issues, @projects)
225 unless User.current.allowed_to?(:copy_issues, @projects)
227 raise ::Unauthorized
226 raise ::Unauthorized
228 end
227 end
229 end
228 end
230
229
231 @allowed_projects = Issue.allowed_target_projects
230 @allowed_projects = Issue.allowed_target_projects
232 if params[:issue]
231 if params[:issue]
233 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
232 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
234 if @target_project
233 if @target_project
235 target_projects = [@target_project]
234 target_projects = [@target_project]
236 end
235 end
237 end
236 end
238 target_projects ||= @projects
237 target_projects ||= @projects
239
238
240 if @copy
239 if @copy
241 # Copied issues will get their default statuses
240 # Copied issues will get their default statuses
242 @available_statuses = []
241 @available_statuses = []
243 else
242 else
244 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
243 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
245 end
244 end
246 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
245 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
247 @assignables = target_projects.map(&:assignable_users).reduce(:&)
246 @assignables = target_projects.map(&:assignable_users).reduce(:&)
248 @trackers = target_projects.map(&:trackers).reduce(:&)
247 @trackers = target_projects.map(&:trackers).reduce(:&)
249 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
248 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
250 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
249 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
251 if @copy
250 if @copy
252 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
251 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
253 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
252 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
254 end
253 end
255
254
256 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
255 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
257
256
258 @issue_params = params[:issue] || {}
257 @issue_params = params[:issue] || {}
259 @issue_params[:custom_field_values] ||= {}
258 @issue_params[:custom_field_values] ||= {}
260 end
259 end
261
260
262 def bulk_update
261 def bulk_update
263 @issues.sort!
262 @issues.sort!
264 @copy = params[:copy].present?
263 @copy = params[:copy].present?
265 attributes = parse_params_for_bulk_issue_attributes(params)
264 attributes = parse_params_for_bulk_issue_attributes(params)
266
265
267 if @copy
266 if @copy
268 unless User.current.allowed_to?(:copy_issues, @projects)
267 unless User.current.allowed_to?(:copy_issues, @projects)
269 raise ::Unauthorized
268 raise ::Unauthorized
270 end
269 end
271 target_projects = @projects
270 target_projects = @projects
272 if attributes['project_id'].present?
271 if attributes['project_id'].present?
273 target_projects = Project.where(:id => attributes['project_id']).to_a
272 target_projects = Project.where(:id => attributes['project_id']).to_a
274 end
273 end
275 unless User.current.allowed_to?(:add_issues, target_projects)
274 unless User.current.allowed_to?(:add_issues, target_projects)
276 raise ::Unauthorized
275 raise ::Unauthorized
277 end
276 end
278 end
277 end
279
278
280 unsaved_issues = []
279 unsaved_issues = []
281 saved_issues = []
280 saved_issues = []
282
281
283 if @copy && params[:copy_subtasks].present?
282 if @copy && params[:copy_subtasks].present?
284 # Descendant issues will be copied with the parent task
283 # Descendant issues will be copied with the parent task
285 # Don't copy them twice
284 # Don't copy them twice
286 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
285 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
287 end
286 end
288
287
289 @issues.each do |orig_issue|
288 @issues.each do |orig_issue|
290 orig_issue.reload
289 orig_issue.reload
291 if @copy
290 if @copy
292 issue = orig_issue.copy({},
291 issue = orig_issue.copy({},
293 :attachments => params[:copy_attachments].present?,
292 :attachments => params[:copy_attachments].present?,
294 :subtasks => params[:copy_subtasks].present?,
293 :subtasks => params[:copy_subtasks].present?,
295 :link => link_copy?(params[:link_copy])
294 :link => link_copy?(params[:link_copy])
296 )
295 )
297 else
296 else
298 issue = orig_issue
297 issue = orig_issue
299 end
298 end
300 journal = issue.init_journal(User.current, params[:notes])
299 journal = issue.init_journal(User.current, params[:notes])
301 issue.safe_attributes = attributes
300 issue.safe_attributes = attributes
302 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
301 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
303 if issue.save
302 if issue.save
304 saved_issues << issue
303 saved_issues << issue
305 else
304 else
306 unsaved_issues << orig_issue
305 unsaved_issues << orig_issue
307 end
306 end
308 end
307 end
309
308
310 if unsaved_issues.empty?
309 if unsaved_issues.empty?
311 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
310 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
312 if params[:follow]
311 if params[:follow]
313 if @issues.size == 1 && saved_issues.size == 1
312 if @issues.size == 1 && saved_issues.size == 1
314 redirect_to issue_path(saved_issues.first)
313 redirect_to issue_path(saved_issues.first)
315 elsif saved_issues.map(&:project).uniq.size == 1
314 elsif saved_issues.map(&:project).uniq.size == 1
316 redirect_to project_issues_path(saved_issues.map(&:project).first)
315 redirect_to project_issues_path(saved_issues.map(&:project).first)
317 end
316 end
318 else
317 else
319 redirect_back_or_default _project_issues_path(@project)
318 redirect_back_or_default _project_issues_path(@project)
320 end
319 end
321 else
320 else
322 @saved_issues = @issues
321 @saved_issues = @issues
323 @unsaved_issues = unsaved_issues
322 @unsaved_issues = unsaved_issues
324 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
323 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
325 bulk_edit
324 bulk_edit
326 render :action => 'bulk_edit'
325 render :action => 'bulk_edit'
327 end
326 end
328 end
327 end
329
328
330 def destroy
329 def destroy
331 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
330 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
332 if @hours > 0
331 if @hours > 0
333 case params[:todo]
332 case params[:todo]
334 when 'destroy'
333 when 'destroy'
335 # nothing to do
334 # nothing to do
336 when 'nullify'
335 when 'nullify'
337 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
336 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
338 when 'reassign'
337 when 'reassign'
339 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
338 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
340 if reassign_to.nil?
339 if reassign_to.nil?
341 flash.now[:error] = l(:error_issue_not_found_in_project)
340 flash.now[:error] = l(:error_issue_not_found_in_project)
342 return
341 return
343 else
342 else
344 TimeEntry.where(['issue_id IN (?)', @issues]).
343 TimeEntry.where(['issue_id IN (?)', @issues]).
345 update_all("issue_id = #{reassign_to.id}")
344 update_all("issue_id = #{reassign_to.id}")
346 end
345 end
347 else
346 else
348 # display the destroy form if it's a user request
347 # display the destroy form if it's a user request
349 return unless api_request?
348 return unless api_request?
350 end
349 end
351 end
350 end
352 @issues.each do |issue|
351 @issues.each do |issue|
353 begin
352 begin
354 issue.reload.destroy
353 issue.reload.destroy
355 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
354 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
356 # nothing to do, issue was already deleted (eg. by a parent)
355 # nothing to do, issue was already deleted (eg. by a parent)
357 end
356 end
358 end
357 end
359 respond_to do |format|
358 respond_to do |format|
360 format.html { redirect_back_or_default _project_issues_path(@project) }
359 format.html { redirect_back_or_default _project_issues_path(@project) }
361 format.api { render_api_ok }
360 format.api { render_api_ok }
362 end
361 end
363 end
362 end
364
363
365 private
364 private
366
365
367 def find_project
366 def find_project
368 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
367 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
369 @project = Project.find(project_id)
368 @project = Project.find(project_id)
370 rescue ActiveRecord::RecordNotFound
369 rescue ActiveRecord::RecordNotFound
371 render_404
370 render_404
372 end
371 end
373
372
374 def retrieve_previous_and_next_issue_ids
373 def retrieve_previous_and_next_issue_ids
375 retrieve_query_from_session
374 retrieve_query_from_session
376 if @query
375 if @query
377 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
376 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
378 sort_update(@query.sortable_columns, 'issues_index_sort')
377 sort_update(@query.sortable_columns, 'issues_index_sort')
379 limit = 500
378 limit = 500
380 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
379 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
381 if (idx = issue_ids.index(@issue.id)) && idx < limit
380 if (idx = issue_ids.index(@issue.id)) && idx < limit
382 if issue_ids.size < 500
381 if issue_ids.size < 500
383 @issue_position = idx + 1
382 @issue_position = idx + 1
384 @issue_count = issue_ids.size
383 @issue_count = issue_ids.size
385 end
384 end
386 @prev_issue_id = issue_ids[idx - 1] if idx > 0
385 @prev_issue_id = issue_ids[idx - 1] if idx > 0
387 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
386 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
388 end
387 end
389 end
388 end
390 end
389 end
391
390
392 # Used by #edit and #update to set some common instance variables
391 # Used by #edit and #update to set some common instance variables
393 # from the params
392 # from the params
394 # TODO: Refactor, not everything in here is needed by #edit
393 # TODO: Refactor, not everything in here is needed by #edit
395 def update_issue_from_params
394 def update_issue_from_params
396 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
397 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
395 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
398 if params[:time_entry]
396 if params[:time_entry]
399 @time_entry.attributes = params[:time_entry]
397 @time_entry.attributes = params[:time_entry]
400 end
398 end
401
399
402 @issue.init_journal(User.current)
400 @issue.init_journal(User.current)
403
401
404 issue_attributes = params[:issue]
402 issue_attributes = params[:issue]
405 if issue_attributes && params[:conflict_resolution]
403 if issue_attributes && params[:conflict_resolution]
406 case params[:conflict_resolution]
404 case params[:conflict_resolution]
407 when 'overwrite'
405 when 'overwrite'
408 issue_attributes = issue_attributes.dup
406 issue_attributes = issue_attributes.dup
409 issue_attributes.delete(:lock_version)
407 issue_attributes.delete(:lock_version)
410 when 'add_notes'
408 when 'add_notes'
411 issue_attributes = issue_attributes.slice(:notes)
409 issue_attributes = issue_attributes.slice(:notes)
412 when 'cancel'
410 when 'cancel'
413 redirect_to issue_path(@issue)
411 redirect_to issue_path(@issue)
414 return false
412 return false
415 end
413 end
416 end
414 end
417 @issue.safe_attributes = issue_attributes
415 @issue.safe_attributes = issue_attributes
418 @priorities = IssuePriority.active
416 @priorities = IssuePriority.active
419 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
417 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
420 true
418 true
421 end
419 end
422
420
423 # TODO: Refactor, lots of extra code in here
421 # TODO: Refactor, lots of extra code in here
424 # TODO: Changing tracker on an existing issue should not trigger this
422 # TODO: Changing tracker on an existing issue should not trigger this
425 def build_new_issue_from_params
423 def build_new_issue_from_params
426 if params[:id].blank?
424 if params[:id].blank?
427 @issue = Issue.new
425 @issue = Issue.new
428 if params[:copy_from]
426 if params[:copy_from]
429 begin
427 begin
430 @issue.init_journal(User.current)
428 @issue.init_journal(User.current)
431 @copy_from = Issue.visible.find(params[:copy_from])
429 @copy_from = Issue.visible.find(params[:copy_from])
432 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
430 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
433 raise ::Unauthorized
431 raise ::Unauthorized
434 end
432 end
435 @link_copy = link_copy?(params[:link_copy]) || request.get?
433 @link_copy = link_copy?(params[:link_copy]) || request.get?
436 @copy_attachments = params[:copy_attachments].present? || request.get?
434 @copy_attachments = params[:copy_attachments].present? || request.get?
437 @copy_subtasks = params[:copy_subtasks].present? || request.get?
435 @copy_subtasks = params[:copy_subtasks].present? || request.get?
438 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
436 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
439 rescue ActiveRecord::RecordNotFound
437 rescue ActiveRecord::RecordNotFound
440 render_404
438 render_404
441 return
439 return
442 end
440 end
443 end
441 end
444 @issue.project = @project
442 @issue.project = @project
445 @issue.author ||= User.current
443 @issue.author ||= User.current
446 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
444 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
447 else
445 else
448 @issue = @project.issues.visible.find(params[:id])
446 @issue = @project.issues.visible.find(params[:id])
449 end
447 end
450
448
451 if attrs = params[:issue].deep_dup
449 if attrs = params[:issue].deep_dup
452 if params[:was_default_status] == attrs[:status_id]
450 if params[:was_default_status] == attrs[:status_id]
453 attrs.delete(:status_id)
451 attrs.delete(:status_id)
454 end
452 end
455 @issue.safe_attributes = attrs
453 @issue.safe_attributes = attrs
456 end
454 end
457 @issue.tracker ||= @project.trackers.first
455 @issue.tracker ||= @project.trackers.first
458 if @issue.tracker.nil?
456 if @issue.tracker.nil?
459 render_error l(:error_no_tracker_in_project)
457 render_error l(:error_no_tracker_in_project)
460 return false
458 return false
461 end
459 end
462 if @issue.status.nil?
460 if @issue.status.nil?
463 render_error l(:error_no_default_issue_status)
461 render_error l(:error_no_default_issue_status)
464 return false
462 return false
465 end
463 end
466
464
467 @priorities = IssuePriority.active
465 @priorities = IssuePriority.active
468 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, @issue.new_record?)
466 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, @issue.new_record?)
469 end
467 end
470
468
471 def parse_params_for_bulk_issue_attributes(params)
469 def parse_params_for_bulk_issue_attributes(params)
472 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
470 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
473 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
471 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
474 if custom = attributes[:custom_field_values]
472 if custom = attributes[:custom_field_values]
475 custom.reject! {|k,v| v.blank?}
473 custom.reject! {|k,v| v.blank?}
476 custom.keys.each do |k|
474 custom.keys.each do |k|
477 if custom[k].is_a?(Array)
475 if custom[k].is_a?(Array)
478 custom[k] << '' if custom[k].delete('__none__')
476 custom[k] << '' if custom[k].delete('__none__')
479 else
477 else
480 custom[k] = '' if custom[k] == '__none__'
478 custom[k] = '' if custom[k] == '__none__'
481 end
479 end
482 end
480 end
483 end
481 end
484 attributes
482 attributes
485 end
483 end
486
484
487 # Saves @issue and a time_entry from the parameters
485 # Saves @issue and a time_entry from the parameters
488 def save_issue_with_child_records
486 def save_issue_with_child_records
489 Issue.transaction do
487 Issue.transaction do
490 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
488 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
491 time_entry = @time_entry || TimeEntry.new
489 time_entry = @time_entry || TimeEntry.new
492 time_entry.project = @issue.project
490 time_entry.project = @issue.project
493 time_entry.issue = @issue
491 time_entry.issue = @issue
494 time_entry.user = User.current
492 time_entry.user = User.current
495 time_entry.spent_on = User.current.today
493 time_entry.spent_on = User.current.today
496 time_entry.attributes = params[:time_entry]
494 time_entry.attributes = params[:time_entry]
497 @issue.time_entries << time_entry
495 @issue.time_entries << time_entry
498 end
496 end
499
497
500 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
498 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
501 if @issue.save
499 if @issue.save
502 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
500 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
503 else
501 else
504 raise ActiveRecord::Rollback
502 raise ActiveRecord::Rollback
505 end
503 end
506 end
504 end
507 end
505 end
508
506
509 def link_copy?(param)
507 def link_copy?(param)
510 case Setting.link_copied_issue
508 case Setting.link_copied_issue
511 when 'yes'
509 when 'yes'
512 true
510 true
513 when 'no'
511 when 'no'
514 false
512 false
515 when 'ask'
513 when 'ask'
516 param == '1'
514 param == '1'
517 end
515 end
518 end
516 end
519 end
517 end
@@ -1,1565 +1,1570
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 include Redmine::Utils::DateCalculation
20 include Redmine::Utils::DateCalculation
21 include Redmine::I18n
21 include Redmine::I18n
22 before_save :set_parent_id
22 before_save :set_parent_id
23 include Redmine::NestedSet::IssueNestedSet
23 include Redmine::NestedSet::IssueNestedSet
24
24
25 belongs_to :project
25 belongs_to :project
26 belongs_to :tracker
26 belongs_to :tracker
27 belongs_to :status, :class_name => 'IssueStatus'
27 belongs_to :status, :class_name => 'IssueStatus'
28 belongs_to :author, :class_name => 'User'
28 belongs_to :author, :class_name => 'User'
29 belongs_to :assigned_to, :class_name => 'Principal'
29 belongs_to :assigned_to, :class_name => 'Principal'
30 belongs_to :fixed_version, :class_name => 'Version'
30 belongs_to :fixed_version, :class_name => 'Version'
31 belongs_to :priority, :class_name => 'IssuePriority'
31 belongs_to :priority, :class_name => 'IssuePriority'
32 belongs_to :category, :class_name => 'IssueCategory'
32 belongs_to :category, :class_name => 'IssueCategory'
33
33
34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
35 has_many :visible_journals,
35 has_many :visible_journals,
36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
37 :class_name => 'Journal',
37 :class_name => 'Journal',
38 :as => :journalized
38 :as => :journalized
39
39
40 has_many :time_entries, :dependent => :destroy
40 has_many :time_entries, :dependent => :destroy
41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
42
42
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45
45
46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 acts_as_customizable
47 acts_as_customizable
48 acts_as_watchable
48 acts_as_watchable
49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
50 :preload => [:project, :status, :tracker],
50 :preload => [:project, :status, :tracker],
51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
52
52
53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56
56
57 acts_as_activity_provider :scope => preload(:project, :author, :tracker),
57 acts_as_activity_provider :scope => preload(:project, :author, :tracker),
58 :author_key => :author_id
58 :author_key => :author_id
59
59
60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61
61
62 attr_reader :current_journal
62 attr_reader :current_journal
63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64
64
65 validates_presence_of :subject, :project, :tracker
65 validates_presence_of :subject, :project, :tracker
66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69
69
70 validates_length_of :subject, :maximum => 255
70 validates_length_of :subject, :maximum => 255
71 validates_inclusion_of :done_ratio, :in => 0..100
71 validates_inclusion_of :done_ratio, :in => 0..100
72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 validates :start_date, :date => true
73 validates :start_date, :date => true
74 validates :due_date, :date => true
74 validates :due_date, :date => true
75 validate :validate_issue, :validate_required_fields
75 validate :validate_issue, :validate_required_fields
76 attr_protected :id
76 attr_protected :id
77
77
78 scope :visible, lambda {|*args|
78 scope :visible, lambda {|*args|
79 joins(:project).
79 joins(:project).
80 where(Issue.visible_condition(args.shift || User.current, *args))
80 where(Issue.visible_condition(args.shift || User.current, *args))
81 }
81 }
82
82
83 scope :open, lambda {|*args|
83 scope :open, lambda {|*args|
84 is_closed = args.size > 0 ? !args.first : false
84 is_closed = args.size > 0 ? !args.first : false
85 joins(:status).
85 joins(:status).
86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
87 }
87 }
88
88
89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
90 scope :on_active_project, lambda {
90 scope :on_active_project, lambda {
91 joins(:project).
91 joins(:project).
92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
93 }
93 }
94 scope :fixed_version, lambda {|versions|
94 scope :fixed_version, lambda {|versions|
95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
97 }
97 }
98
98
99 before_create :default_assign
99 before_create :default_assign
100 before_save :close_duplicates, :update_done_ratio_from_issue_status,
100 before_save :close_duplicates, :update_done_ratio_from_issue_status,
101 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
101 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
102 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
102 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
103 after_save :reschedule_following_issues, :update_nested_set_attributes,
103 after_save :reschedule_following_issues, :update_nested_set_attributes,
104 :update_parent_attributes, :create_journal
104 :update_parent_attributes, :create_journal
105 # Should be after_create but would be called before previous after_save callbacks
105 # Should be after_create but would be called before previous after_save callbacks
106 after_save :after_create_from_copy
106 after_save :after_create_from_copy
107 after_destroy :update_parent_attributes
107 after_destroy :update_parent_attributes
108 after_create :send_notification
108 after_create :send_notification
109 # Keep it at the end of after_save callbacks
109 # Keep it at the end of after_save callbacks
110 after_save :clear_assigned_to_was
110 after_save :clear_assigned_to_was
111
111
112 # Returns a SQL conditions string used to find all issues visible by the specified user
112 # Returns a SQL conditions string used to find all issues visible by the specified user
113 def self.visible_condition(user, options={})
113 def self.visible_condition(user, options={})
114 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
114 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
115 if user.id && user.logged?
115 if user.id && user.logged?
116 case role.issues_visibility
116 case role.issues_visibility
117 when 'all'
117 when 'all'
118 nil
118 nil
119 when 'default'
119 when 'default'
120 user_ids = [user.id] + user.groups.map(&:id).compact
120 user_ids = [user.id] + user.groups.map(&:id).compact
121 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
121 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
122 when 'own'
122 when 'own'
123 user_ids = [user.id] + user.groups.map(&:id).compact
123 user_ids = [user.id] + user.groups.map(&:id).compact
124 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
124 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
125 else
125 else
126 '1=0'
126 '1=0'
127 end
127 end
128 else
128 else
129 "(#{table_name}.is_private = #{connection.quoted_false})"
129 "(#{table_name}.is_private = #{connection.quoted_false})"
130 end
130 end
131 end
131 end
132 end
132 end
133
133
134 # Returns true if usr or current user is allowed to view the issue
134 # Returns true if usr or current user is allowed to view the issue
135 def visible?(usr=nil)
135 def visible?(usr=nil)
136 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
136 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
137 if user.logged?
137 if user.logged?
138 case role.issues_visibility
138 case role.issues_visibility
139 when 'all'
139 when 'all'
140 true
140 true
141 when 'default'
141 when 'default'
142 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
142 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
143 when 'own'
143 when 'own'
144 self.author == user || user.is_or_belongs_to?(assigned_to)
144 self.author == user || user.is_or_belongs_to?(assigned_to)
145 else
145 else
146 false
146 false
147 end
147 end
148 else
148 else
149 !self.is_private?
149 !self.is_private?
150 end
150 end
151 end
151 end
152 end
152 end
153
153
154 # Returns true if user or current user is allowed to edit or add a note to the issue
154 # Returns true if user or current user is allowed to edit or add a note to the issue
155 def editable?(user=User.current)
155 def editable?(user=User.current)
156 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
156 attributes_editable?(user) || user.allowed_to?(:add_issue_notes, project)
157 end
158
159 # Returns true if user or current user is allowed to edit the issue
160 def attributes_editable?(user=User.current)
161 user.allowed_to?(:edit_issues, project)
157 end
162 end
158
163
159 def initialize(attributes=nil, *args)
164 def initialize(attributes=nil, *args)
160 super
165 super
161 if new_record?
166 if new_record?
162 # set default values for new records only
167 # set default values for new records only
163 self.priority ||= IssuePriority.default
168 self.priority ||= IssuePriority.default
164 self.watcher_user_ids = []
169 self.watcher_user_ids = []
165 end
170 end
166 end
171 end
167
172
168 def create_or_update
173 def create_or_update
169 super
174 super
170 ensure
175 ensure
171 @status_was = nil
176 @status_was = nil
172 end
177 end
173 private :create_or_update
178 private :create_or_update
174
179
175 # AR#Persistence#destroy would raise and RecordNotFound exception
180 # AR#Persistence#destroy would raise and RecordNotFound exception
176 # if the issue was already deleted or updated (non matching lock_version).
181 # if the issue was already deleted or updated (non matching lock_version).
177 # This is a problem when bulk deleting issues or deleting a project
182 # This is a problem when bulk deleting issues or deleting a project
178 # (because an issue may already be deleted if its parent was deleted
183 # (because an issue may already be deleted if its parent was deleted
179 # first).
184 # first).
180 # The issue is reloaded by the nested_set before being deleted so
185 # The issue is reloaded by the nested_set before being deleted so
181 # the lock_version condition should not be an issue but we handle it.
186 # the lock_version condition should not be an issue but we handle it.
182 def destroy
187 def destroy
183 super
188 super
184 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
189 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
185 # Stale or already deleted
190 # Stale or already deleted
186 begin
191 begin
187 reload
192 reload
188 rescue ActiveRecord::RecordNotFound
193 rescue ActiveRecord::RecordNotFound
189 # The issue was actually already deleted
194 # The issue was actually already deleted
190 @destroyed = true
195 @destroyed = true
191 return freeze
196 return freeze
192 end
197 end
193 # The issue was stale, retry to destroy
198 # The issue was stale, retry to destroy
194 super
199 super
195 end
200 end
196
201
197 alias :base_reload :reload
202 alias :base_reload :reload
198 def reload(*args)
203 def reload(*args)
199 @workflow_rule_by_attribute = nil
204 @workflow_rule_by_attribute = nil
200 @assignable_versions = nil
205 @assignable_versions = nil
201 @relations = nil
206 @relations = nil
202 @spent_hours = nil
207 @spent_hours = nil
203 base_reload(*args)
208 base_reload(*args)
204 end
209 end
205
210
206 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
211 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
207 def available_custom_fields
212 def available_custom_fields
208 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
213 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
209 end
214 end
210
215
211 def visible_custom_field_values(user=nil)
216 def visible_custom_field_values(user=nil)
212 user_real = user || User.current
217 user_real = user || User.current
213 custom_field_values.select do |value|
218 custom_field_values.select do |value|
214 value.custom_field.visible_by?(project, user_real)
219 value.custom_field.visible_by?(project, user_real)
215 end
220 end
216 end
221 end
217
222
218 # Copies attributes from another issue, arg can be an id or an Issue
223 # Copies attributes from another issue, arg can be an id or an Issue
219 def copy_from(arg, options={})
224 def copy_from(arg, options={})
220 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
225 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
221 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
226 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
222 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
227 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
223 self.status = issue.status
228 self.status = issue.status
224 self.author = User.current
229 self.author = User.current
225 unless options[:attachments] == false
230 unless options[:attachments] == false
226 self.attachments = issue.attachments.map do |attachement|
231 self.attachments = issue.attachments.map do |attachement|
227 attachement.copy(:container => self)
232 attachement.copy(:container => self)
228 end
233 end
229 end
234 end
230 @copied_from = issue
235 @copied_from = issue
231 @copy_options = options
236 @copy_options = options
232 self
237 self
233 end
238 end
234
239
235 # Returns an unsaved copy of the issue
240 # Returns an unsaved copy of the issue
236 def copy(attributes=nil, copy_options={})
241 def copy(attributes=nil, copy_options={})
237 copy = self.class.new.copy_from(self, copy_options)
242 copy = self.class.new.copy_from(self, copy_options)
238 copy.attributes = attributes if attributes
243 copy.attributes = attributes if attributes
239 copy
244 copy
240 end
245 end
241
246
242 # Returns true if the issue is a copy
247 # Returns true if the issue is a copy
243 def copy?
248 def copy?
244 @copied_from.present?
249 @copied_from.present?
245 end
250 end
246
251
247 def status_id=(status_id)
252 def status_id=(status_id)
248 if status_id.to_s != self.status_id.to_s
253 if status_id.to_s != self.status_id.to_s
249 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
254 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
250 end
255 end
251 self.status_id
256 self.status_id
252 end
257 end
253
258
254 # Sets the status.
259 # Sets the status.
255 def status=(status)
260 def status=(status)
256 if status != self.status
261 if status != self.status
257 @workflow_rule_by_attribute = nil
262 @workflow_rule_by_attribute = nil
258 end
263 end
259 association(:status).writer(status)
264 association(:status).writer(status)
260 end
265 end
261
266
262 def priority_id=(pid)
267 def priority_id=(pid)
263 self.priority = nil
268 self.priority = nil
264 write_attribute(:priority_id, pid)
269 write_attribute(:priority_id, pid)
265 end
270 end
266
271
267 def category_id=(cid)
272 def category_id=(cid)
268 self.category = nil
273 self.category = nil
269 write_attribute(:category_id, cid)
274 write_attribute(:category_id, cid)
270 end
275 end
271
276
272 def fixed_version_id=(vid)
277 def fixed_version_id=(vid)
273 self.fixed_version = nil
278 self.fixed_version = nil
274 write_attribute(:fixed_version_id, vid)
279 write_attribute(:fixed_version_id, vid)
275 end
280 end
276
281
277 def tracker_id=(tracker_id)
282 def tracker_id=(tracker_id)
278 if tracker_id.to_s != self.tracker_id.to_s
283 if tracker_id.to_s != self.tracker_id.to_s
279 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
284 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
280 end
285 end
281 self.tracker_id
286 self.tracker_id
282 end
287 end
283
288
284 # Sets the tracker.
289 # Sets the tracker.
285 # This will set the status to the default status of the new tracker if:
290 # This will set the status to the default status of the new tracker if:
286 # * the status was the default for the previous tracker
291 # * the status was the default for the previous tracker
287 # * or if the status was not part of the new tracker statuses
292 # * or if the status was not part of the new tracker statuses
288 # * or the status was nil
293 # * or the status was nil
289 def tracker=(tracker)
294 def tracker=(tracker)
290 if tracker != self.tracker
295 if tracker != self.tracker
291 if status == default_status
296 if status == default_status
292 self.status = nil
297 self.status = nil
293 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
298 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
294 self.status = nil
299 self.status = nil
295 end
300 end
296 @custom_field_values = nil
301 @custom_field_values = nil
297 @workflow_rule_by_attribute = nil
302 @workflow_rule_by_attribute = nil
298 end
303 end
299 association(:tracker).writer(tracker)
304 association(:tracker).writer(tracker)
300 self.status ||= default_status
305 self.status ||= default_status
301 self.tracker
306 self.tracker
302 end
307 end
303
308
304 def project_id=(project_id)
309 def project_id=(project_id)
305 if project_id.to_s != self.project_id.to_s
310 if project_id.to_s != self.project_id.to_s
306 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
311 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
307 end
312 end
308 self.project_id
313 self.project_id
309 end
314 end
310
315
311 # Sets the project.
316 # Sets the project.
312 # Unless keep_tracker argument is set to true, this will change the tracker
317 # Unless keep_tracker argument is set to true, this will change the tracker
313 # to the first tracker of the new project if the previous tracker is not part
318 # to the first tracker of the new project if the previous tracker is not part
314 # of the new project trackers.
319 # of the new project trackers.
315 # This will clear the fixed_version is it's no longer valid for the new project.
320 # This will clear the fixed_version is it's no longer valid for the new project.
316 # This will clear the parent issue if it's no longer valid for the new project.
321 # This will clear the parent issue if it's no longer valid for the new project.
317 # This will set the category to the category with the same name in the new
322 # This will set the category to the category with the same name in the new
318 # project if it exists, or clear it if it doesn't.
323 # project if it exists, or clear it if it doesn't.
319 def project=(project, keep_tracker=false)
324 def project=(project, keep_tracker=false)
320 project_was = self.project
325 project_was = self.project
321 association(:project).writer(project)
326 association(:project).writer(project)
322 if project_was && project && project_was != project
327 if project_was && project && project_was != project
323 @assignable_versions = nil
328 @assignable_versions = nil
324
329
325 unless keep_tracker || project.trackers.include?(tracker)
330 unless keep_tracker || project.trackers.include?(tracker)
326 self.tracker = project.trackers.first
331 self.tracker = project.trackers.first
327 end
332 end
328 # Reassign to the category with same name if any
333 # Reassign to the category with same name if any
329 if category
334 if category
330 self.category = project.issue_categories.find_by_name(category.name)
335 self.category = project.issue_categories.find_by_name(category.name)
331 end
336 end
332 # Keep the fixed_version if it's still valid in the new_project
337 # Keep the fixed_version if it's still valid in the new_project
333 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
338 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
334 self.fixed_version = nil
339 self.fixed_version = nil
335 end
340 end
336 # Clear the parent task if it's no longer valid
341 # Clear the parent task if it's no longer valid
337 unless valid_parent_project?
342 unless valid_parent_project?
338 self.parent_issue_id = nil
343 self.parent_issue_id = nil
339 end
344 end
340 @custom_field_values = nil
345 @custom_field_values = nil
341 @workflow_rule_by_attribute = nil
346 @workflow_rule_by_attribute = nil
342 end
347 end
343 self.project
348 self.project
344 end
349 end
345
350
346 def description=(arg)
351 def description=(arg)
347 if arg.is_a?(String)
352 if arg.is_a?(String)
348 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
353 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
349 end
354 end
350 write_attribute(:description, arg)
355 write_attribute(:description, arg)
351 end
356 end
352
357
353 # Overrides assign_attributes so that project and tracker get assigned first
358 # Overrides assign_attributes so that project and tracker get assigned first
354 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
359 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
355 return if new_attributes.nil?
360 return if new_attributes.nil?
356 attrs = new_attributes.dup
361 attrs = new_attributes.dup
357 attrs.stringify_keys!
362 attrs.stringify_keys!
358
363
359 %w(project project_id tracker tracker_id).each do |attr|
364 %w(project project_id tracker tracker_id).each do |attr|
360 if attrs.has_key?(attr)
365 if attrs.has_key?(attr)
361 send "#{attr}=", attrs.delete(attr)
366 send "#{attr}=", attrs.delete(attr)
362 end
367 end
363 end
368 end
364 send :assign_attributes_without_project_and_tracker_first, attrs, *args
369 send :assign_attributes_without_project_and_tracker_first, attrs, *args
365 end
370 end
366 # Do not redefine alias chain on reload (see #4838)
371 # Do not redefine alias chain on reload (see #4838)
367 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
372 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
368
373
369 def attributes=(new_attributes)
374 def attributes=(new_attributes)
370 assign_attributes new_attributes
375 assign_attributes new_attributes
371 end
376 end
372
377
373 def estimated_hours=(h)
378 def estimated_hours=(h)
374 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
379 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
375 end
380 end
376
381
377 safe_attributes 'project_id',
382 safe_attributes 'project_id',
378 :if => lambda {|issue, user|
383 :if => lambda {|issue, user|
379 if issue.new_record?
384 if issue.new_record?
380 issue.copy?
385 issue.copy?
381 else
386 else
382 user.allowed_to?(:edit_issues, issue.project)
387 user.allowed_to?(:edit_issues, issue.project)
383 end
388 end
384 }
389 }
385
390
386 safe_attributes 'tracker_id',
391 safe_attributes 'tracker_id',
387 'status_id',
392 'status_id',
388 'category_id',
393 'category_id',
389 'assigned_to_id',
394 'assigned_to_id',
390 'priority_id',
395 'priority_id',
391 'fixed_version_id',
396 'fixed_version_id',
392 'subject',
397 'subject',
393 'description',
398 'description',
394 'start_date',
399 'start_date',
395 'due_date',
400 'due_date',
396 'done_ratio',
401 'done_ratio',
397 'estimated_hours',
402 'estimated_hours',
398 'custom_field_values',
403 'custom_field_values',
399 'custom_fields',
404 'custom_fields',
400 'lock_version',
405 'lock_version',
401 'notes',
406 'notes',
402 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
407 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
403
408
404 safe_attributes 'notes',
409 safe_attributes 'notes',
405 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
410 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
406
411
407 safe_attributes 'private_notes',
412 safe_attributes 'private_notes',
408 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
413 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
409
414
410 safe_attributes 'watcher_user_ids',
415 safe_attributes 'watcher_user_ids',
411 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
416 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
412
417
413 safe_attributes 'is_private',
418 safe_attributes 'is_private',
414 :if => lambda {|issue, user|
419 :if => lambda {|issue, user|
415 user.allowed_to?(:set_issues_private, issue.project) ||
420 user.allowed_to?(:set_issues_private, issue.project) ||
416 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
421 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
417 }
422 }
418
423
419 safe_attributes 'parent_issue_id',
424 safe_attributes 'parent_issue_id',
420 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
425 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
421 user.allowed_to?(:manage_subtasks, issue.project)}
426 user.allowed_to?(:manage_subtasks, issue.project)}
422
427
423 def safe_attribute_names(user=nil)
428 def safe_attribute_names(user=nil)
424 names = super
429 names = super
425 names -= disabled_core_fields
430 names -= disabled_core_fields
426 names -= read_only_attribute_names(user)
431 names -= read_only_attribute_names(user)
427 if new_record? && copy?
432 if new_record? && copy?
428 names |= %w(project_id)
433 names |= %w(project_id)
429 end
434 end
430 names
435 names
431 end
436 end
432
437
433 # Safely sets attributes
438 # Safely sets attributes
434 # Should be called from controllers instead of #attributes=
439 # Should be called from controllers instead of #attributes=
435 # attr_accessible is too rough because we still want things like
440 # attr_accessible is too rough because we still want things like
436 # Issue.new(:project => foo) to work
441 # Issue.new(:project => foo) to work
437 def safe_attributes=(attrs, user=User.current)
442 def safe_attributes=(attrs, user=User.current)
438 return unless attrs.is_a?(Hash)
443 return unless attrs.is_a?(Hash)
439
444
440 attrs = attrs.deep_dup
445 attrs = attrs.deep_dup
441
446
442 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
447 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
443 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
448 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
444 if allowed_target_projects(user).where(:id => p.to_i).exists?
449 if allowed_target_projects(user).where(:id => p.to_i).exists?
445 self.project_id = p
450 self.project_id = p
446 end
451 end
447 end
452 end
448
453
449 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
454 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
450 self.tracker_id = t
455 self.tracker_id = t
451 end
456 end
452
457
453 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
458 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
454 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
459 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
455 self.status_id = s
460 self.status_id = s
456 end
461 end
457 end
462 end
458
463
459 attrs = delete_unsafe_attributes(attrs, user)
464 attrs = delete_unsafe_attributes(attrs, user)
460 return if attrs.empty?
465 return if attrs.empty?
461
466
462 unless leaf?
467 unless leaf?
463 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
468 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
464 end
469 end
465
470
466 if attrs['parent_issue_id'].present?
471 if attrs['parent_issue_id'].present?
467 s = attrs['parent_issue_id'].to_s
472 s = attrs['parent_issue_id'].to_s
468 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
473 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
469 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
474 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
470 end
475 end
471 end
476 end
472
477
473 if attrs['custom_field_values'].present?
478 if attrs['custom_field_values'].present?
474 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
479 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
475 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
480 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
476 end
481 end
477
482
478 if attrs['custom_fields'].present?
483 if attrs['custom_fields'].present?
479 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
484 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
480 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
485 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
481 end
486 end
482
487
483 # mass-assignment security bypass
488 # mass-assignment security bypass
484 assign_attributes attrs, :without_protection => true
489 assign_attributes attrs, :without_protection => true
485 end
490 end
486
491
487 def disabled_core_fields
492 def disabled_core_fields
488 tracker ? tracker.disabled_core_fields : []
493 tracker ? tracker.disabled_core_fields : []
489 end
494 end
490
495
491 # Returns the custom_field_values that can be edited by the given user
496 # Returns the custom_field_values that can be edited by the given user
492 def editable_custom_field_values(user=nil)
497 def editable_custom_field_values(user=nil)
493 visible_custom_field_values(user).reject do |value|
498 visible_custom_field_values(user).reject do |value|
494 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
499 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
495 end
500 end
496 end
501 end
497
502
498 # Returns the custom fields that can be edited by the given user
503 # Returns the custom fields that can be edited by the given user
499 def editable_custom_fields(user=nil)
504 def editable_custom_fields(user=nil)
500 editable_custom_field_values(user).map(&:custom_field).uniq
505 editable_custom_field_values(user).map(&:custom_field).uniq
501 end
506 end
502
507
503 # Returns the names of attributes that are read-only for user or the current user
508 # Returns the names of attributes that are read-only for user or the current user
504 # For users with multiple roles, the read-only fields are the intersection of
509 # For users with multiple roles, the read-only fields are the intersection of
505 # read-only fields of each role
510 # read-only fields of each role
506 # The result is an array of strings where sustom fields are represented with their ids
511 # The result is an array of strings where sustom fields are represented with their ids
507 #
512 #
508 # Examples:
513 # Examples:
509 # issue.read_only_attribute_names # => ['due_date', '2']
514 # issue.read_only_attribute_names # => ['due_date', '2']
510 # issue.read_only_attribute_names(user) # => []
515 # issue.read_only_attribute_names(user) # => []
511 def read_only_attribute_names(user=nil)
516 def read_only_attribute_names(user=nil)
512 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
517 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
513 end
518 end
514
519
515 # Returns the names of required attributes for user or the current user
520 # Returns the names of required attributes for user or the current user
516 # For users with multiple roles, the required fields are the intersection of
521 # For users with multiple roles, the required fields are the intersection of
517 # required fields of each role
522 # required fields of each role
518 # The result is an array of strings where sustom fields are represented with their ids
523 # The result is an array of strings where sustom fields are represented with their ids
519 #
524 #
520 # Examples:
525 # Examples:
521 # issue.required_attribute_names # => ['due_date', '2']
526 # issue.required_attribute_names # => ['due_date', '2']
522 # issue.required_attribute_names(user) # => []
527 # issue.required_attribute_names(user) # => []
523 def required_attribute_names(user=nil)
528 def required_attribute_names(user=nil)
524 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
529 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
525 end
530 end
526
531
527 # Returns true if the attribute is required for user
532 # Returns true if the attribute is required for user
528 def required_attribute?(name, user=nil)
533 def required_attribute?(name, user=nil)
529 required_attribute_names(user).include?(name.to_s)
534 required_attribute_names(user).include?(name.to_s)
530 end
535 end
531
536
532 # Returns a hash of the workflow rule by attribute for the given user
537 # Returns a hash of the workflow rule by attribute for the given user
533 #
538 #
534 # Examples:
539 # Examples:
535 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
540 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
536 def workflow_rule_by_attribute(user=nil)
541 def workflow_rule_by_attribute(user=nil)
537 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
542 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
538
543
539 user_real = user || User.current
544 user_real = user || User.current
540 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
545 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
541 roles = roles.select(&:consider_workflow?)
546 roles = roles.select(&:consider_workflow?)
542 return {} if roles.empty?
547 return {} if roles.empty?
543
548
544 result = {}
549 result = {}
545 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
550 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
546 if workflow_permissions.any?
551 if workflow_permissions.any?
547 workflow_rules = workflow_permissions.inject({}) do |h, wp|
552 workflow_rules = workflow_permissions.inject({}) do |h, wp|
548 h[wp.field_name] ||= []
553 h[wp.field_name] ||= []
549 h[wp.field_name] << wp.rule
554 h[wp.field_name] << wp.rule
550 h
555 h
551 end
556 end
552 workflow_rules.each do |attr, rules|
557 workflow_rules.each do |attr, rules|
553 next if rules.size < roles.size
558 next if rules.size < roles.size
554 uniq_rules = rules.uniq
559 uniq_rules = rules.uniq
555 if uniq_rules.size == 1
560 if uniq_rules.size == 1
556 result[attr] = uniq_rules.first
561 result[attr] = uniq_rules.first
557 else
562 else
558 result[attr] = 'required'
563 result[attr] = 'required'
559 end
564 end
560 end
565 end
561 end
566 end
562 @workflow_rule_by_attribute = result if user.nil?
567 @workflow_rule_by_attribute = result if user.nil?
563 result
568 result
564 end
569 end
565 private :workflow_rule_by_attribute
570 private :workflow_rule_by_attribute
566
571
567 def done_ratio
572 def done_ratio
568 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
573 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
569 status.default_done_ratio
574 status.default_done_ratio
570 else
575 else
571 read_attribute(:done_ratio)
576 read_attribute(:done_ratio)
572 end
577 end
573 end
578 end
574
579
575 def self.use_status_for_done_ratio?
580 def self.use_status_for_done_ratio?
576 Setting.issue_done_ratio == 'issue_status'
581 Setting.issue_done_ratio == 'issue_status'
577 end
582 end
578
583
579 def self.use_field_for_done_ratio?
584 def self.use_field_for_done_ratio?
580 Setting.issue_done_ratio == 'issue_field'
585 Setting.issue_done_ratio == 'issue_field'
581 end
586 end
582
587
583 def validate_issue
588 def validate_issue
584 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
589 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
585 errors.add :due_date, :greater_than_start_date
590 errors.add :due_date, :greater_than_start_date
586 end
591 end
587
592
588 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
593 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
589 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
594 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
590 end
595 end
591
596
592 if fixed_version
597 if fixed_version
593 if !assignable_versions.include?(fixed_version)
598 if !assignable_versions.include?(fixed_version)
594 errors.add :fixed_version_id, :inclusion
599 errors.add :fixed_version_id, :inclusion
595 elsif reopening? && fixed_version.closed?
600 elsif reopening? && fixed_version.closed?
596 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
601 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
597 end
602 end
598 end
603 end
599
604
600 # Checks that the issue can not be added/moved to a disabled tracker
605 # Checks that the issue can not be added/moved to a disabled tracker
601 if project && (tracker_id_changed? || project_id_changed?)
606 if project && (tracker_id_changed? || project_id_changed?)
602 unless project.trackers.include?(tracker)
607 unless project.trackers.include?(tracker)
603 errors.add :tracker_id, :inclusion
608 errors.add :tracker_id, :inclusion
604 end
609 end
605 end
610 end
606
611
607 # Checks parent issue assignment
612 # Checks parent issue assignment
608 if @invalid_parent_issue_id.present?
613 if @invalid_parent_issue_id.present?
609 errors.add :parent_issue_id, :invalid
614 errors.add :parent_issue_id, :invalid
610 elsif @parent_issue
615 elsif @parent_issue
611 if !valid_parent_project?(@parent_issue)
616 if !valid_parent_project?(@parent_issue)
612 errors.add :parent_issue_id, :invalid
617 errors.add :parent_issue_id, :invalid
613 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
618 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
614 errors.add :parent_issue_id, :invalid
619 errors.add :parent_issue_id, :invalid
615 elsif !new_record?
620 elsif !new_record?
616 # moving an existing issue
621 # moving an existing issue
617 if move_possible?(@parent_issue)
622 if move_possible?(@parent_issue)
618 # move accepted
623 # move accepted
619 else
624 else
620 errors.add :parent_issue_id, :invalid
625 errors.add :parent_issue_id, :invalid
621 end
626 end
622 end
627 end
623 end
628 end
624 end
629 end
625
630
626 # Validates the issue against additional workflow requirements
631 # Validates the issue against additional workflow requirements
627 def validate_required_fields
632 def validate_required_fields
628 user = new_record? ? author : current_journal.try(:user)
633 user = new_record? ? author : current_journal.try(:user)
629
634
630 required_attribute_names(user).each do |attribute|
635 required_attribute_names(user).each do |attribute|
631 if attribute =~ /^\d+$/
636 if attribute =~ /^\d+$/
632 attribute = attribute.to_i
637 attribute = attribute.to_i
633 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
638 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
634 if v && v.value.blank?
639 if v && v.value.blank?
635 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
640 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
636 end
641 end
637 else
642 else
638 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
643 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
639 errors.add attribute, :blank
644 errors.add attribute, :blank
640 end
645 end
641 end
646 end
642 end
647 end
643 end
648 end
644
649
645 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
650 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
646 # even if the user turns off the setting later
651 # even if the user turns off the setting later
647 def update_done_ratio_from_issue_status
652 def update_done_ratio_from_issue_status
648 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
653 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
649 self.done_ratio = status.default_done_ratio
654 self.done_ratio = status.default_done_ratio
650 end
655 end
651 end
656 end
652
657
653 def init_journal(user, notes = "")
658 def init_journal(user, notes = "")
654 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
659 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
655 end
660 end
656
661
657 # Returns the current journal or nil if it's not initialized
662 # Returns the current journal or nil if it's not initialized
658 def current_journal
663 def current_journal
659 @current_journal
664 @current_journal
660 end
665 end
661
666
662 # Returns the names of attributes that are journalized when updating the issue
667 # Returns the names of attributes that are journalized when updating the issue
663 def journalized_attribute_names
668 def journalized_attribute_names
664 Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
669 Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
665 end
670 end
666
671
667 # Returns the id of the last journal or nil
672 # Returns the id of the last journal or nil
668 def last_journal_id
673 def last_journal_id
669 if new_record?
674 if new_record?
670 nil
675 nil
671 else
676 else
672 journals.maximum(:id)
677 journals.maximum(:id)
673 end
678 end
674 end
679 end
675
680
676 # Returns a scope for journals that have an id greater than journal_id
681 # Returns a scope for journals that have an id greater than journal_id
677 def journals_after(journal_id)
682 def journals_after(journal_id)
678 scope = journals.reorder("#{Journal.table_name}.id ASC")
683 scope = journals.reorder("#{Journal.table_name}.id ASC")
679 if journal_id.present?
684 if journal_id.present?
680 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
685 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
681 end
686 end
682 scope
687 scope
683 end
688 end
684
689
685 # Returns the initial status of the issue
690 # Returns the initial status of the issue
686 # Returns nil for a new issue
691 # Returns nil for a new issue
687 def status_was
692 def status_was
688 if status_id_changed?
693 if status_id_changed?
689 if status_id_was.to_i > 0
694 if status_id_was.to_i > 0
690 @status_was ||= IssueStatus.find_by_id(status_id_was)
695 @status_was ||= IssueStatus.find_by_id(status_id_was)
691 end
696 end
692 else
697 else
693 @status_was ||= status
698 @status_was ||= status
694 end
699 end
695 end
700 end
696
701
697 # Return true if the issue is closed, otherwise false
702 # Return true if the issue is closed, otherwise false
698 def closed?
703 def closed?
699 status.present? && status.is_closed?
704 status.present? && status.is_closed?
700 end
705 end
701
706
702 # Returns true if the issue was closed when loaded
707 # Returns true if the issue was closed when loaded
703 def was_closed?
708 def was_closed?
704 status_was.present? && status_was.is_closed?
709 status_was.present? && status_was.is_closed?
705 end
710 end
706
711
707 # Return true if the issue is being reopened
712 # Return true if the issue is being reopened
708 def reopening?
713 def reopening?
709 if new_record?
714 if new_record?
710 false
715 false
711 else
716 else
712 status_id_changed? && !closed? && was_closed?
717 status_id_changed? && !closed? && was_closed?
713 end
718 end
714 end
719 end
715 alias :reopened? :reopening?
720 alias :reopened? :reopening?
716
721
717 # Return true if the issue is being closed
722 # Return true if the issue is being closed
718 def closing?
723 def closing?
719 if new_record?
724 if new_record?
720 closed?
725 closed?
721 else
726 else
722 status_id_changed? && closed? && !was_closed?
727 status_id_changed? && closed? && !was_closed?
723 end
728 end
724 end
729 end
725
730
726 # Returns true if the issue is overdue
731 # Returns true if the issue is overdue
727 def overdue?
732 def overdue?
728 due_date.present? && (due_date < Date.today) && !closed?
733 due_date.present? && (due_date < Date.today) && !closed?
729 end
734 end
730
735
731 # Is the amount of work done less than it should for the due date
736 # Is the amount of work done less than it should for the due date
732 def behind_schedule?
737 def behind_schedule?
733 return false if start_date.nil? || due_date.nil?
738 return false if start_date.nil? || due_date.nil?
734 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
739 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
735 return done_date <= Date.today
740 return done_date <= Date.today
736 end
741 end
737
742
738 # Does this issue have children?
743 # Does this issue have children?
739 def children?
744 def children?
740 !leaf?
745 !leaf?
741 end
746 end
742
747
743 # Users the issue can be assigned to
748 # Users the issue can be assigned to
744 def assignable_users
749 def assignable_users
745 users = project.assignable_users.to_a
750 users = project.assignable_users.to_a
746 users << author if author
751 users << author if author
747 users << assigned_to if assigned_to
752 users << assigned_to if assigned_to
748 users.uniq.sort
753 users.uniq.sort
749 end
754 end
750
755
751 # Versions that the issue can be assigned to
756 # Versions that the issue can be assigned to
752 def assignable_versions
757 def assignable_versions
753 return @assignable_versions if @assignable_versions
758 return @assignable_versions if @assignable_versions
754
759
755 versions = project.shared_versions.open.to_a
760 versions = project.shared_versions.open.to_a
756 if fixed_version
761 if fixed_version
757 if fixed_version_id_changed?
762 if fixed_version_id_changed?
758 # nothing to do
763 # nothing to do
759 elsif project_id_changed?
764 elsif project_id_changed?
760 if project.shared_versions.include?(fixed_version)
765 if project.shared_versions.include?(fixed_version)
761 versions << fixed_version
766 versions << fixed_version
762 end
767 end
763 else
768 else
764 versions << fixed_version
769 versions << fixed_version
765 end
770 end
766 end
771 end
767 @assignable_versions = versions.uniq.sort
772 @assignable_versions = versions.uniq.sort
768 end
773 end
769
774
770 # Returns true if this issue is blocked by another issue that is still open
775 # Returns true if this issue is blocked by another issue that is still open
771 def blocked?
776 def blocked?
772 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
777 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
773 end
778 end
774
779
775 # Returns the default status of the issue based on its tracker
780 # Returns the default status of the issue based on its tracker
776 # Returns nil if tracker is nil
781 # Returns nil if tracker is nil
777 def default_status
782 def default_status
778 tracker.try(:default_status)
783 tracker.try(:default_status)
779 end
784 end
780
785
781 # Returns an array of statuses that user is able to apply
786 # Returns an array of statuses that user is able to apply
782 def new_statuses_allowed_to(user=User.current, include_default=false)
787 def new_statuses_allowed_to(user=User.current, include_default=false)
783 if new_record? && @copied_from
788 if new_record? && @copied_from
784 [default_status, @copied_from.status].compact.uniq.sort
789 [default_status, @copied_from.status].compact.uniq.sort
785 else
790 else
786 initial_status = nil
791 initial_status = nil
787 if new_record?
792 if new_record?
788 initial_status = default_status
793 initial_status = default_status
789 elsif tracker_id_changed?
794 elsif tracker_id_changed?
790 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
795 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
791 initial_status = default_status
796 initial_status = default_status
792 elsif tracker.issue_status_ids.include?(status_id_was)
797 elsif tracker.issue_status_ids.include?(status_id_was)
793 initial_status = IssueStatus.find_by_id(status_id_was)
798 initial_status = IssueStatus.find_by_id(status_id_was)
794 else
799 else
795 initial_status = default_status
800 initial_status = default_status
796 end
801 end
797 else
802 else
798 initial_status = status_was
803 initial_status = status_was
799 end
804 end
800
805
801 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
806 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
802 assignee_transitions_allowed = initial_assigned_to_id.present? &&
807 assignee_transitions_allowed = initial_assigned_to_id.present? &&
803 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
808 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
804
809
805 statuses = []
810 statuses = []
806 if initial_status
811 if initial_status
807 statuses += initial_status.find_new_statuses_allowed_to(
812 statuses += initial_status.find_new_statuses_allowed_to(
808 user.admin ? Role.all.to_a : user.roles_for_project(project),
813 user.admin ? Role.all.to_a : user.roles_for_project(project),
809 tracker,
814 tracker,
810 author == user,
815 author == user,
811 assignee_transitions_allowed
816 assignee_transitions_allowed
812 )
817 )
813 end
818 end
814 statuses << initial_status unless statuses.empty?
819 statuses << initial_status unless statuses.empty?
815 statuses << default_status if include_default
820 statuses << default_status if include_default
816 statuses = statuses.compact.uniq.sort
821 statuses = statuses.compact.uniq.sort
817 if blocked?
822 if blocked?
818 statuses.reject!(&:is_closed?)
823 statuses.reject!(&:is_closed?)
819 end
824 end
820 statuses
825 statuses
821 end
826 end
822 end
827 end
823
828
824 # Returns the previous assignee if changed
829 # Returns the previous assignee if changed
825 def assigned_to_was
830 def assigned_to_was
826 # assigned_to_id_was is reset before after_save callbacks
831 # assigned_to_id_was is reset before after_save callbacks
827 user_id = @previous_assigned_to_id || assigned_to_id_was
832 user_id = @previous_assigned_to_id || assigned_to_id_was
828 if user_id && user_id != assigned_to_id
833 if user_id && user_id != assigned_to_id
829 @assigned_to_was ||= User.find_by_id(user_id)
834 @assigned_to_was ||= User.find_by_id(user_id)
830 end
835 end
831 end
836 end
832
837
833 # Returns the users that should be notified
838 # Returns the users that should be notified
834 def notified_users
839 def notified_users
835 notified = []
840 notified = []
836 # Author and assignee are always notified unless they have been
841 # Author and assignee are always notified unless they have been
837 # locked or don't want to be notified
842 # locked or don't want to be notified
838 notified << author if author
843 notified << author if author
839 if assigned_to
844 if assigned_to
840 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
845 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
841 end
846 end
842 if assigned_to_was
847 if assigned_to_was
843 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
848 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
844 end
849 end
845 notified = notified.select {|u| u.active? && u.notify_about?(self)}
850 notified = notified.select {|u| u.active? && u.notify_about?(self)}
846
851
847 notified += project.notified_users
852 notified += project.notified_users
848 notified.uniq!
853 notified.uniq!
849 # Remove users that can not view the issue
854 # Remove users that can not view the issue
850 notified.reject! {|user| !visible?(user)}
855 notified.reject! {|user| !visible?(user)}
851 notified
856 notified
852 end
857 end
853
858
854 # Returns the email addresses that should be notified
859 # Returns the email addresses that should be notified
855 def recipients
860 def recipients
856 notified_users.collect(&:mail)
861 notified_users.collect(&:mail)
857 end
862 end
858
863
859 def each_notification(users, &block)
864 def each_notification(users, &block)
860 if users.any?
865 if users.any?
861 if custom_field_values.detect {|value| !value.custom_field.visible?}
866 if custom_field_values.detect {|value| !value.custom_field.visible?}
862 users_by_custom_field_visibility = users.group_by do |user|
867 users_by_custom_field_visibility = users.group_by do |user|
863 visible_custom_field_values(user).map(&:custom_field_id).sort
868 visible_custom_field_values(user).map(&:custom_field_id).sort
864 end
869 end
865 users_by_custom_field_visibility.values.each do |users|
870 users_by_custom_field_visibility.values.each do |users|
866 yield(users)
871 yield(users)
867 end
872 end
868 else
873 else
869 yield(users)
874 yield(users)
870 end
875 end
871 end
876 end
872 end
877 end
873
878
874 # Returns the number of hours spent on this issue
879 # Returns the number of hours spent on this issue
875 def spent_hours
880 def spent_hours
876 @spent_hours ||= time_entries.sum(:hours) || 0
881 @spent_hours ||= time_entries.sum(:hours) || 0
877 end
882 end
878
883
879 # Returns the total number of hours spent on this issue and its descendants
884 # Returns the total number of hours spent on this issue and its descendants
880 #
885 #
881 # Example:
886 # Example:
882 # spent_hours => 0.0
887 # spent_hours => 0.0
883 # spent_hours => 50.2
888 # spent_hours => 50.2
884 def total_spent_hours
889 def total_spent_hours
885 @total_spent_hours ||=
890 @total_spent_hours ||=
886 self_and_descendants.
891 self_and_descendants.
887 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
892 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
888 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
893 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
889 end
894 end
890
895
891 def relations
896 def relations
892 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
897 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
893 end
898 end
894
899
895 # Preloads relations for a collection of issues
900 # Preloads relations for a collection of issues
896 def self.load_relations(issues)
901 def self.load_relations(issues)
897 if issues.any?
902 if issues.any?
898 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
903 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
899 issues.each do |issue|
904 issues.each do |issue|
900 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
905 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
901 end
906 end
902 end
907 end
903 end
908 end
904
909
905 # Preloads visible spent time for a collection of issues
910 # Preloads visible spent time for a collection of issues
906 def self.load_visible_spent_hours(issues, user=User.current)
911 def self.load_visible_spent_hours(issues, user=User.current)
907 if issues.any?
912 if issues.any?
908 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
913 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
909 issues.each do |issue|
914 issues.each do |issue|
910 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
915 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
911 end
916 end
912 end
917 end
913 end
918 end
914
919
915 # Preloads visible relations for a collection of issues
920 # Preloads visible relations for a collection of issues
916 def self.load_visible_relations(issues, user=User.current)
921 def self.load_visible_relations(issues, user=User.current)
917 if issues.any?
922 if issues.any?
918 issue_ids = issues.map(&:id)
923 issue_ids = issues.map(&:id)
919 # Relations with issue_from in given issues and visible issue_to
924 # Relations with issue_from in given issues and visible issue_to
920 relations_from = IssueRelation.joins(:issue_to => :project).
925 relations_from = IssueRelation.joins(:issue_to => :project).
921 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
926 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
922 # Relations with issue_to in given issues and visible issue_from
927 # Relations with issue_to in given issues and visible issue_from
923 relations_to = IssueRelation.joins(:issue_from => :project).
928 relations_to = IssueRelation.joins(:issue_from => :project).
924 where(visible_condition(user)).
929 where(visible_condition(user)).
925 where(:issue_to_id => issue_ids).to_a
930 where(:issue_to_id => issue_ids).to_a
926 issues.each do |issue|
931 issues.each do |issue|
927 relations =
932 relations =
928 relations_from.select {|relation| relation.issue_from_id == issue.id} +
933 relations_from.select {|relation| relation.issue_from_id == issue.id} +
929 relations_to.select {|relation| relation.issue_to_id == issue.id}
934 relations_to.select {|relation| relation.issue_to_id == issue.id}
930
935
931 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
936 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
932 end
937 end
933 end
938 end
934 end
939 end
935
940
936 # Finds an issue relation given its id.
941 # Finds an issue relation given its id.
937 def find_relation(relation_id)
942 def find_relation(relation_id)
938 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
943 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
939 end
944 end
940
945
941 # Returns all the other issues that depend on the issue
946 # Returns all the other issues that depend on the issue
942 # The algorithm is a modified breadth first search (bfs)
947 # The algorithm is a modified breadth first search (bfs)
943 def all_dependent_issues(except=[])
948 def all_dependent_issues(except=[])
944 # The found dependencies
949 # The found dependencies
945 dependencies = []
950 dependencies = []
946
951
947 # The visited flag for every node (issue) used by the breadth first search
952 # The visited flag for every node (issue) used by the breadth first search
948 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
953 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
949
954
950 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
955 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
951 # the issue when it is processed.
956 # the issue when it is processed.
952
957
953 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
958 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
954 # but its children will not be added to the queue when it is processed.
959 # but its children will not be added to the queue when it is processed.
955
960
956 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
961 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
957 # the queue, but its children have not been added.
962 # the queue, but its children have not been added.
958
963
959 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
964 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
960 # the children still need to be processed.
965 # the children still need to be processed.
961
966
962 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
967 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
963 # added as dependent issues. It needs no further processing.
968 # added as dependent issues. It needs no further processing.
964
969
965 issue_status = Hash.new(eNOT_DISCOVERED)
970 issue_status = Hash.new(eNOT_DISCOVERED)
966
971
967 # The queue
972 # The queue
968 queue = []
973 queue = []
969
974
970 # Initialize the bfs, add start node (self) to the queue
975 # Initialize the bfs, add start node (self) to the queue
971 queue << self
976 queue << self
972 issue_status[self] = ePROCESS_ALL
977 issue_status[self] = ePROCESS_ALL
973
978
974 while (!queue.empty?) do
979 while (!queue.empty?) do
975 current_issue = queue.shift
980 current_issue = queue.shift
976 current_issue_status = issue_status[current_issue]
981 current_issue_status = issue_status[current_issue]
977 dependencies << current_issue
982 dependencies << current_issue
978
983
979 # Add parent to queue, if not already in it.
984 # Add parent to queue, if not already in it.
980 parent = current_issue.parent
985 parent = current_issue.parent
981 parent_status = issue_status[parent]
986 parent_status = issue_status[parent]
982
987
983 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
988 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
984 queue << parent
989 queue << parent
985 issue_status[parent] = ePROCESS_RELATIONS_ONLY
990 issue_status[parent] = ePROCESS_RELATIONS_ONLY
986 end
991 end
987
992
988 # Add children to queue, but only if they are not already in it and
993 # Add children to queue, but only if they are not already in it and
989 # the children of the current node need to be processed.
994 # the children of the current node need to be processed.
990 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
995 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
991 current_issue.children.each do |child|
996 current_issue.children.each do |child|
992 next if except.include?(child)
997 next if except.include?(child)
993
998
994 if (issue_status[child] == eNOT_DISCOVERED)
999 if (issue_status[child] == eNOT_DISCOVERED)
995 queue << child
1000 queue << child
996 issue_status[child] = ePROCESS_ALL
1001 issue_status[child] = ePROCESS_ALL
997 elsif (issue_status[child] == eRELATIONS_PROCESSED)
1002 elsif (issue_status[child] == eRELATIONS_PROCESSED)
998 queue << child
1003 queue << child
999 issue_status[child] = ePROCESS_CHILDREN_ONLY
1004 issue_status[child] = ePROCESS_CHILDREN_ONLY
1000 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
1005 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
1001 queue << child
1006 queue << child
1002 issue_status[child] = ePROCESS_ALL
1007 issue_status[child] = ePROCESS_ALL
1003 end
1008 end
1004 end
1009 end
1005 end
1010 end
1006
1011
1007 # Add related issues to the queue, if they are not already in it.
1012 # Add related issues to the queue, if they are not already in it.
1008 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1013 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1009 next if except.include?(related_issue)
1014 next if except.include?(related_issue)
1010
1015
1011 if (issue_status[related_issue] == eNOT_DISCOVERED)
1016 if (issue_status[related_issue] == eNOT_DISCOVERED)
1012 queue << related_issue
1017 queue << related_issue
1013 issue_status[related_issue] = ePROCESS_ALL
1018 issue_status[related_issue] = ePROCESS_ALL
1014 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1019 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1015 queue << related_issue
1020 queue << related_issue
1016 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1021 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1017 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1022 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1018 queue << related_issue
1023 queue << related_issue
1019 issue_status[related_issue] = ePROCESS_ALL
1024 issue_status[related_issue] = ePROCESS_ALL
1020 end
1025 end
1021 end
1026 end
1022
1027
1023 # Set new status for current issue
1028 # Set new status for current issue
1024 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1029 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1025 issue_status[current_issue] = eALL_PROCESSED
1030 issue_status[current_issue] = eALL_PROCESSED
1026 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1031 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1027 issue_status[current_issue] = eRELATIONS_PROCESSED
1032 issue_status[current_issue] = eRELATIONS_PROCESSED
1028 end
1033 end
1029 end # while
1034 end # while
1030
1035
1031 # Remove the issues from the "except" parameter from the result array
1036 # Remove the issues from the "except" parameter from the result array
1032 dependencies -= except
1037 dependencies -= except
1033 dependencies.delete(self)
1038 dependencies.delete(self)
1034
1039
1035 dependencies
1040 dependencies
1036 end
1041 end
1037
1042
1038 # Returns an array of issues that duplicate this one
1043 # Returns an array of issues that duplicate this one
1039 def duplicates
1044 def duplicates
1040 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1045 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1041 end
1046 end
1042
1047
1043 # Returns the due date or the target due date if any
1048 # Returns the due date or the target due date if any
1044 # Used on gantt chart
1049 # Used on gantt chart
1045 def due_before
1050 def due_before
1046 due_date || (fixed_version ? fixed_version.effective_date : nil)
1051 due_date || (fixed_version ? fixed_version.effective_date : nil)
1047 end
1052 end
1048
1053
1049 # Returns the time scheduled for this issue.
1054 # Returns the time scheduled for this issue.
1050 #
1055 #
1051 # Example:
1056 # Example:
1052 # Start Date: 2/26/09, End Date: 3/04/09
1057 # Start Date: 2/26/09, End Date: 3/04/09
1053 # duration => 6
1058 # duration => 6
1054 def duration
1059 def duration
1055 (start_date && due_date) ? due_date - start_date : 0
1060 (start_date && due_date) ? due_date - start_date : 0
1056 end
1061 end
1057
1062
1058 # Returns the duration in working days
1063 # Returns the duration in working days
1059 def working_duration
1064 def working_duration
1060 (start_date && due_date) ? working_days(start_date, due_date) : 0
1065 (start_date && due_date) ? working_days(start_date, due_date) : 0
1061 end
1066 end
1062
1067
1063 def soonest_start(reload=false)
1068 def soonest_start(reload=false)
1064 @soonest_start = nil if reload
1069 @soonest_start = nil if reload
1065 @soonest_start ||= (
1070 @soonest_start ||= (
1066 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1071 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1067 [(@parent_issue || parent).try(:soonest_start)]
1072 [(@parent_issue || parent).try(:soonest_start)]
1068 ).compact.max
1073 ).compact.max
1069 end
1074 end
1070
1075
1071 # Sets start_date on the given date or the next working day
1076 # Sets start_date on the given date or the next working day
1072 # and changes due_date to keep the same working duration.
1077 # and changes due_date to keep the same working duration.
1073 def reschedule_on(date)
1078 def reschedule_on(date)
1074 wd = working_duration
1079 wd = working_duration
1075 date = next_working_date(date)
1080 date = next_working_date(date)
1076 self.start_date = date
1081 self.start_date = date
1077 self.due_date = add_working_days(date, wd)
1082 self.due_date = add_working_days(date, wd)
1078 end
1083 end
1079
1084
1080 # Reschedules the issue on the given date or the next working day and saves the record.
1085 # Reschedules the issue on the given date or the next working day and saves the record.
1081 # If the issue is a parent task, this is done by rescheduling its subtasks.
1086 # If the issue is a parent task, this is done by rescheduling its subtasks.
1082 def reschedule_on!(date)
1087 def reschedule_on!(date)
1083 return if date.nil?
1088 return if date.nil?
1084 if leaf?
1089 if leaf?
1085 if start_date.nil? || start_date != date
1090 if start_date.nil? || start_date != date
1086 if start_date && start_date > date
1091 if start_date && start_date > date
1087 # Issue can not be moved earlier than its soonest start date
1092 # Issue can not be moved earlier than its soonest start date
1088 date = [soonest_start(true), date].compact.max
1093 date = [soonest_start(true), date].compact.max
1089 end
1094 end
1090 reschedule_on(date)
1095 reschedule_on(date)
1091 begin
1096 begin
1092 save
1097 save
1093 rescue ActiveRecord::StaleObjectError
1098 rescue ActiveRecord::StaleObjectError
1094 reload
1099 reload
1095 reschedule_on(date)
1100 reschedule_on(date)
1096 save
1101 save
1097 end
1102 end
1098 end
1103 end
1099 else
1104 else
1100 leaves.each do |leaf|
1105 leaves.each do |leaf|
1101 if leaf.start_date
1106 if leaf.start_date
1102 # Only move subtask if it starts at the same date as the parent
1107 # Only move subtask if it starts at the same date as the parent
1103 # or if it starts before the given date
1108 # or if it starts before the given date
1104 if start_date == leaf.start_date || date > leaf.start_date
1109 if start_date == leaf.start_date || date > leaf.start_date
1105 leaf.reschedule_on!(date)
1110 leaf.reschedule_on!(date)
1106 end
1111 end
1107 else
1112 else
1108 leaf.reschedule_on!(date)
1113 leaf.reschedule_on!(date)
1109 end
1114 end
1110 end
1115 end
1111 end
1116 end
1112 end
1117 end
1113
1118
1114 def <=>(issue)
1119 def <=>(issue)
1115 if issue.nil?
1120 if issue.nil?
1116 -1
1121 -1
1117 elsif root_id != issue.root_id
1122 elsif root_id != issue.root_id
1118 (root_id || 0) <=> (issue.root_id || 0)
1123 (root_id || 0) <=> (issue.root_id || 0)
1119 else
1124 else
1120 (lft || 0) <=> (issue.lft || 0)
1125 (lft || 0) <=> (issue.lft || 0)
1121 end
1126 end
1122 end
1127 end
1123
1128
1124 def to_s
1129 def to_s
1125 "#{tracker} ##{id}: #{subject}"
1130 "#{tracker} ##{id}: #{subject}"
1126 end
1131 end
1127
1132
1128 # Returns a string of css classes that apply to the issue
1133 # Returns a string of css classes that apply to the issue
1129 def css_classes(user=User.current)
1134 def css_classes(user=User.current)
1130 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1135 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1131 s << ' closed' if closed?
1136 s << ' closed' if closed?
1132 s << ' overdue' if overdue?
1137 s << ' overdue' if overdue?
1133 s << ' child' if child?
1138 s << ' child' if child?
1134 s << ' parent' unless leaf?
1139 s << ' parent' unless leaf?
1135 s << ' private' if is_private?
1140 s << ' private' if is_private?
1136 if user.logged?
1141 if user.logged?
1137 s << ' created-by-me' if author_id == user.id
1142 s << ' created-by-me' if author_id == user.id
1138 s << ' assigned-to-me' if assigned_to_id == user.id
1143 s << ' assigned-to-me' if assigned_to_id == user.id
1139 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1144 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1140 end
1145 end
1141 s
1146 s
1142 end
1147 end
1143
1148
1144 # Unassigns issues from +version+ if it's no longer shared with issue's project
1149 # Unassigns issues from +version+ if it's no longer shared with issue's project
1145 def self.update_versions_from_sharing_change(version)
1150 def self.update_versions_from_sharing_change(version)
1146 # Update issues assigned to the version
1151 # Update issues assigned to the version
1147 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1152 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1148 end
1153 end
1149
1154
1150 # Unassigns issues from versions that are no longer shared
1155 # Unassigns issues from versions that are no longer shared
1151 # after +project+ was moved
1156 # after +project+ was moved
1152 def self.update_versions_from_hierarchy_change(project)
1157 def self.update_versions_from_hierarchy_change(project)
1153 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1158 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1154 # Update issues of the moved projects and issues assigned to a version of a moved project
1159 # Update issues of the moved projects and issues assigned to a version of a moved project
1155 Issue.update_versions(
1160 Issue.update_versions(
1156 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1161 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1157 moved_project_ids, moved_project_ids]
1162 moved_project_ids, moved_project_ids]
1158 )
1163 )
1159 end
1164 end
1160
1165
1161 def parent_issue_id=(arg)
1166 def parent_issue_id=(arg)
1162 s = arg.to_s.strip.presence
1167 s = arg.to_s.strip.presence
1163 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1168 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1164 @invalid_parent_issue_id = nil
1169 @invalid_parent_issue_id = nil
1165 elsif s.blank?
1170 elsif s.blank?
1166 @parent_issue = nil
1171 @parent_issue = nil
1167 @invalid_parent_issue_id = nil
1172 @invalid_parent_issue_id = nil
1168 else
1173 else
1169 @parent_issue = nil
1174 @parent_issue = nil
1170 @invalid_parent_issue_id = arg
1175 @invalid_parent_issue_id = arg
1171 end
1176 end
1172 end
1177 end
1173
1178
1174 def parent_issue_id
1179 def parent_issue_id
1175 if @invalid_parent_issue_id
1180 if @invalid_parent_issue_id
1176 @invalid_parent_issue_id
1181 @invalid_parent_issue_id
1177 elsif instance_variable_defined? :@parent_issue
1182 elsif instance_variable_defined? :@parent_issue
1178 @parent_issue.nil? ? nil : @parent_issue.id
1183 @parent_issue.nil? ? nil : @parent_issue.id
1179 else
1184 else
1180 parent_id
1185 parent_id
1181 end
1186 end
1182 end
1187 end
1183
1188
1184 def set_parent_id
1189 def set_parent_id
1185 self.parent_id = parent_issue_id
1190 self.parent_id = parent_issue_id
1186 end
1191 end
1187
1192
1188 # Returns true if issue's project is a valid
1193 # Returns true if issue's project is a valid
1189 # parent issue project
1194 # parent issue project
1190 def valid_parent_project?(issue=parent)
1195 def valid_parent_project?(issue=parent)
1191 return true if issue.nil? || issue.project_id == project_id
1196 return true if issue.nil? || issue.project_id == project_id
1192
1197
1193 case Setting.cross_project_subtasks
1198 case Setting.cross_project_subtasks
1194 when 'system'
1199 when 'system'
1195 true
1200 true
1196 when 'tree'
1201 when 'tree'
1197 issue.project.root == project.root
1202 issue.project.root == project.root
1198 when 'hierarchy'
1203 when 'hierarchy'
1199 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1204 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1200 when 'descendants'
1205 when 'descendants'
1201 issue.project.is_or_is_ancestor_of?(project)
1206 issue.project.is_or_is_ancestor_of?(project)
1202 else
1207 else
1203 false
1208 false
1204 end
1209 end
1205 end
1210 end
1206
1211
1207 # Returns an issue scope based on project and scope
1212 # Returns an issue scope based on project and scope
1208 def self.cross_project_scope(project, scope=nil)
1213 def self.cross_project_scope(project, scope=nil)
1209 if project.nil?
1214 if project.nil?
1210 return Issue
1215 return Issue
1211 end
1216 end
1212 case scope
1217 case scope
1213 when 'all', 'system'
1218 when 'all', 'system'
1214 Issue
1219 Issue
1215 when 'tree'
1220 when 'tree'
1216 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1221 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1217 :lft => project.root.lft, :rgt => project.root.rgt)
1222 :lft => project.root.lft, :rgt => project.root.rgt)
1218 when 'hierarchy'
1223 when 'hierarchy'
1219 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1224 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1220 :lft => project.lft, :rgt => project.rgt)
1225 :lft => project.lft, :rgt => project.rgt)
1221 when 'descendants'
1226 when 'descendants'
1222 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1227 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1223 :lft => project.lft, :rgt => project.rgt)
1228 :lft => project.lft, :rgt => project.rgt)
1224 else
1229 else
1225 Issue.where(:project_id => project.id)
1230 Issue.where(:project_id => project.id)
1226 end
1231 end
1227 end
1232 end
1228
1233
1229 def self.by_tracker(project)
1234 def self.by_tracker(project)
1230 count_and_group_by(:project => project, :association => :tracker)
1235 count_and_group_by(:project => project, :association => :tracker)
1231 end
1236 end
1232
1237
1233 def self.by_version(project)
1238 def self.by_version(project)
1234 count_and_group_by(:project => project, :association => :fixed_version)
1239 count_and_group_by(:project => project, :association => :fixed_version)
1235 end
1240 end
1236
1241
1237 def self.by_priority(project)
1242 def self.by_priority(project)
1238 count_and_group_by(:project => project, :association => :priority)
1243 count_and_group_by(:project => project, :association => :priority)
1239 end
1244 end
1240
1245
1241 def self.by_category(project)
1246 def self.by_category(project)
1242 count_and_group_by(:project => project, :association => :category)
1247 count_and_group_by(:project => project, :association => :category)
1243 end
1248 end
1244
1249
1245 def self.by_assigned_to(project)
1250 def self.by_assigned_to(project)
1246 count_and_group_by(:project => project, :association => :assigned_to)
1251 count_and_group_by(:project => project, :association => :assigned_to)
1247 end
1252 end
1248
1253
1249 def self.by_author(project)
1254 def self.by_author(project)
1250 count_and_group_by(:project => project, :association => :author)
1255 count_and_group_by(:project => project, :association => :author)
1251 end
1256 end
1252
1257
1253 def self.by_subproject(project)
1258 def self.by_subproject(project)
1254 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1259 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1255 r.reject {|r| r["project_id"] == project.id.to_s}
1260 r.reject {|r| r["project_id"] == project.id.to_s}
1256 end
1261 end
1257
1262
1258 # Query generator for selecting groups of issue counts for a project
1263 # Query generator for selecting groups of issue counts for a project
1259 # based on specific criteria
1264 # based on specific criteria
1260 #
1265 #
1261 # Options
1266 # Options
1262 # * project - Project to search in.
1267 # * project - Project to search in.
1263 # * with_subprojects - Includes subprojects issues if set to true.
1268 # * with_subprojects - Includes subprojects issues if set to true.
1264 # * association - Symbol. Association for grouping.
1269 # * association - Symbol. Association for grouping.
1265 def self.count_and_group_by(options)
1270 def self.count_and_group_by(options)
1266 assoc = reflect_on_association(options[:association])
1271 assoc = reflect_on_association(options[:association])
1267 select_field = assoc.foreign_key
1272 select_field = assoc.foreign_key
1268
1273
1269 Issue.
1274 Issue.
1270 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1275 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1271 joins(:status, assoc.name).
1276 joins(:status, assoc.name).
1272 group(:status_id, :is_closed, select_field).
1277 group(:status_id, :is_closed, select_field).
1273 count.
1278 count.
1274 map do |columns, total|
1279 map do |columns, total|
1275 status_id, is_closed, field_value = columns
1280 status_id, is_closed, field_value = columns
1276 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1281 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1277 {
1282 {
1278 "status_id" => status_id.to_s,
1283 "status_id" => status_id.to_s,
1279 "closed" => is_closed,
1284 "closed" => is_closed,
1280 select_field => field_value.to_s,
1285 select_field => field_value.to_s,
1281 "total" => total.to_s
1286 "total" => total.to_s
1282 }
1287 }
1283 end
1288 end
1284 end
1289 end
1285
1290
1286 # Returns a scope of projects that user can assign the issue to
1291 # Returns a scope of projects that user can assign the issue to
1287 def allowed_target_projects(user=User.current)
1292 def allowed_target_projects(user=User.current)
1288 current_project = new_record? ? nil : project
1293 current_project = new_record? ? nil : project
1289 self.class.allowed_target_projects(user, current_project)
1294 self.class.allowed_target_projects(user, current_project)
1290 end
1295 end
1291
1296
1292 # Returns a scope of projects that user can assign issues to
1297 # Returns a scope of projects that user can assign issues to
1293 # If current_project is given, it will be included in the scope
1298 # If current_project is given, it will be included in the scope
1294 def self.allowed_target_projects(user=User.current, current_project=nil)
1299 def self.allowed_target_projects(user=User.current, current_project=nil)
1295 condition = Project.allowed_to_condition(user, :add_issues)
1300 condition = Project.allowed_to_condition(user, :add_issues)
1296 if current_project
1301 if current_project
1297 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1302 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1298 end
1303 end
1299 Project.where(condition)
1304 Project.where(condition)
1300 end
1305 end
1301
1306
1302 private
1307 private
1303
1308
1304 def after_project_change
1309 def after_project_change
1305 # Update project_id on related time entries
1310 # Update project_id on related time entries
1306 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1311 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1307
1312
1308 # Delete issue relations
1313 # Delete issue relations
1309 unless Setting.cross_project_issue_relations?
1314 unless Setting.cross_project_issue_relations?
1310 relations_from.clear
1315 relations_from.clear
1311 relations_to.clear
1316 relations_to.clear
1312 end
1317 end
1313
1318
1314 # Move subtasks that were in the same project
1319 # Move subtasks that were in the same project
1315 children.each do |child|
1320 children.each do |child|
1316 next unless child.project_id == project_id_was
1321 next unless child.project_id == project_id_was
1317 # Change project and keep project
1322 # Change project and keep project
1318 child.send :project=, project, true
1323 child.send :project=, project, true
1319 unless child.save
1324 unless child.save
1320 raise ActiveRecord::Rollback
1325 raise ActiveRecord::Rollback
1321 end
1326 end
1322 end
1327 end
1323 end
1328 end
1324
1329
1325 # Callback for after the creation of an issue by copy
1330 # Callback for after the creation of an issue by copy
1326 # * adds a "copied to" relation with the copied issue
1331 # * adds a "copied to" relation with the copied issue
1327 # * copies subtasks from the copied issue
1332 # * copies subtasks from the copied issue
1328 def after_create_from_copy
1333 def after_create_from_copy
1329 return unless copy? && !@after_create_from_copy_handled
1334 return unless copy? && !@after_create_from_copy_handled
1330
1335
1331 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1336 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1332 if @current_journal
1337 if @current_journal
1333 @copied_from.init_journal(@current_journal.user)
1338 @copied_from.init_journal(@current_journal.user)
1334 end
1339 end
1335 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1340 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1336 unless relation.save
1341 unless relation.save
1337 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1342 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1338 end
1343 end
1339 end
1344 end
1340
1345
1341 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1346 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1342 copy_options = (@copy_options || {}).merge(:subtasks => false)
1347 copy_options = (@copy_options || {}).merge(:subtasks => false)
1343 copied_issue_ids = {@copied_from.id => self.id}
1348 copied_issue_ids = {@copied_from.id => self.id}
1344 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1349 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1345 # Do not copy self when copying an issue as a descendant of the copied issue
1350 # Do not copy self when copying an issue as a descendant of the copied issue
1346 next if child == self
1351 next if child == self
1347 # Do not copy subtasks of issues that were not copied
1352 # Do not copy subtasks of issues that were not copied
1348 next unless copied_issue_ids[child.parent_id]
1353 next unless copied_issue_ids[child.parent_id]
1349 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1354 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1350 unless child.visible?
1355 unless child.visible?
1351 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1356 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1352 next
1357 next
1353 end
1358 end
1354 copy = Issue.new.copy_from(child, copy_options)
1359 copy = Issue.new.copy_from(child, copy_options)
1355 if @current_journal
1360 if @current_journal
1356 copy.init_journal(@current_journal.user)
1361 copy.init_journal(@current_journal.user)
1357 end
1362 end
1358 copy.author = author
1363 copy.author = author
1359 copy.project = project
1364 copy.project = project
1360 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1365 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1361 unless copy.save
1366 unless copy.save
1362 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1367 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1363 next
1368 next
1364 end
1369 end
1365 copied_issue_ids[child.id] = copy.id
1370 copied_issue_ids[child.id] = copy.id
1366 end
1371 end
1367 end
1372 end
1368 @after_create_from_copy_handled = true
1373 @after_create_from_copy_handled = true
1369 end
1374 end
1370
1375
1371 def update_nested_set_attributes
1376 def update_nested_set_attributes
1372 if parent_id_changed?
1377 if parent_id_changed?
1373 update_nested_set_attributes_on_parent_change
1378 update_nested_set_attributes_on_parent_change
1374 end
1379 end
1375 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1380 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1376 end
1381 end
1377
1382
1378 # Updates the nested set for when an existing issue is moved
1383 # Updates the nested set for when an existing issue is moved
1379 def update_nested_set_attributes_on_parent_change
1384 def update_nested_set_attributes_on_parent_change
1380 former_parent_id = parent_id_was
1385 former_parent_id = parent_id_was
1381 # delete invalid relations of all descendants
1386 # delete invalid relations of all descendants
1382 self_and_descendants.each do |issue|
1387 self_and_descendants.each do |issue|
1383 issue.relations.each do |relation|
1388 issue.relations.each do |relation|
1384 relation.destroy unless relation.valid?
1389 relation.destroy unless relation.valid?
1385 end
1390 end
1386 end
1391 end
1387 # update former parent
1392 # update former parent
1388 recalculate_attributes_for(former_parent_id) if former_parent_id
1393 recalculate_attributes_for(former_parent_id) if former_parent_id
1389 end
1394 end
1390
1395
1391 def update_parent_attributes
1396 def update_parent_attributes
1392 if parent_id
1397 if parent_id
1393 recalculate_attributes_for(parent_id)
1398 recalculate_attributes_for(parent_id)
1394 association(:parent).reset
1399 association(:parent).reset
1395 end
1400 end
1396 end
1401 end
1397
1402
1398 def recalculate_attributes_for(issue_id)
1403 def recalculate_attributes_for(issue_id)
1399 if issue_id && p = Issue.find_by_id(issue_id)
1404 if issue_id && p = Issue.find_by_id(issue_id)
1400 # priority = highest priority of children
1405 # priority = highest priority of children
1401 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1406 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1402 p.priority = IssuePriority.find_by_position(priority_position)
1407 p.priority = IssuePriority.find_by_position(priority_position)
1403 end
1408 end
1404
1409
1405 # start/due dates = lowest/highest dates of children
1410 # start/due dates = lowest/highest dates of children
1406 p.start_date = p.children.minimum(:start_date)
1411 p.start_date = p.children.minimum(:start_date)
1407 p.due_date = p.children.maximum(:due_date)
1412 p.due_date = p.children.maximum(:due_date)
1408 if p.start_date && p.due_date && p.due_date < p.start_date
1413 if p.start_date && p.due_date && p.due_date < p.start_date
1409 p.start_date, p.due_date = p.due_date, p.start_date
1414 p.start_date, p.due_date = p.due_date, p.start_date
1410 end
1415 end
1411
1416
1412 # done ratio = weighted average ratio of leaves
1417 # done ratio = weighted average ratio of leaves
1413 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1418 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1414 leaves_count = p.leaves.count
1419 leaves_count = p.leaves.count
1415 if leaves_count > 0
1420 if leaves_count > 0
1416 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1421 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1417 if average == 0
1422 if average == 0
1418 average = 1
1423 average = 1
1419 end
1424 end
1420 done = p.leaves.joins(:status).
1425 done = p.leaves.joins(:status).
1421 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1426 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1422 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1427 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1423 progress = done / (average * leaves_count)
1428 progress = done / (average * leaves_count)
1424 p.done_ratio = progress.round
1429 p.done_ratio = progress.round
1425 end
1430 end
1426 end
1431 end
1427
1432
1428 # estimate = sum of leaves estimates
1433 # estimate = sum of leaves estimates
1429 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1434 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1430 p.estimated_hours = nil if p.estimated_hours == 0.0
1435 p.estimated_hours = nil if p.estimated_hours == 0.0
1431
1436
1432 # ancestors will be recursively updated
1437 # ancestors will be recursively updated
1433 p.save(:validate => false)
1438 p.save(:validate => false)
1434 end
1439 end
1435 end
1440 end
1436
1441
1437 # Update issues so their versions are not pointing to a
1442 # Update issues so their versions are not pointing to a
1438 # fixed_version that is not shared with the issue's project
1443 # fixed_version that is not shared with the issue's project
1439 def self.update_versions(conditions=nil)
1444 def self.update_versions(conditions=nil)
1440 # Only need to update issues with a fixed_version from
1445 # Only need to update issues with a fixed_version from
1441 # a different project and that is not systemwide shared
1446 # a different project and that is not systemwide shared
1442 Issue.joins(:project, :fixed_version).
1447 Issue.joins(:project, :fixed_version).
1443 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1448 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1444 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1449 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1445 " AND #{Version.table_name}.sharing <> 'system'").
1450 " AND #{Version.table_name}.sharing <> 'system'").
1446 where(conditions).each do |issue|
1451 where(conditions).each do |issue|
1447 next if issue.project.nil? || issue.fixed_version.nil?
1452 next if issue.project.nil? || issue.fixed_version.nil?
1448 unless issue.project.shared_versions.include?(issue.fixed_version)
1453 unless issue.project.shared_versions.include?(issue.fixed_version)
1449 issue.init_journal(User.current)
1454 issue.init_journal(User.current)
1450 issue.fixed_version = nil
1455 issue.fixed_version = nil
1451 issue.save
1456 issue.save
1452 end
1457 end
1453 end
1458 end
1454 end
1459 end
1455
1460
1456 # Callback on file attachment
1461 # Callback on file attachment
1457 def attachment_added(attachment)
1462 def attachment_added(attachment)
1458 if current_journal && !attachment.new_record?
1463 if current_journal && !attachment.new_record?
1459 current_journal.journalize_attachment(attachment, :added)
1464 current_journal.journalize_attachment(attachment, :added)
1460 end
1465 end
1461 end
1466 end
1462
1467
1463 # Callback on attachment deletion
1468 # Callback on attachment deletion
1464 def attachment_removed(attachment)
1469 def attachment_removed(attachment)
1465 if current_journal && !attachment.new_record?
1470 if current_journal && !attachment.new_record?
1466 current_journal.journalize_attachment(attachment, :removed)
1471 current_journal.journalize_attachment(attachment, :removed)
1467 current_journal.save
1472 current_journal.save
1468 end
1473 end
1469 end
1474 end
1470
1475
1471 # Called after a relation is added
1476 # Called after a relation is added
1472 def relation_added(relation)
1477 def relation_added(relation)
1473 if current_journal
1478 if current_journal
1474 current_journal.journalize_relation(relation, :added)
1479 current_journal.journalize_relation(relation, :added)
1475 current_journal.save
1480 current_journal.save
1476 end
1481 end
1477 end
1482 end
1478
1483
1479 # Called after a relation is removed
1484 # Called after a relation is removed
1480 def relation_removed(relation)
1485 def relation_removed(relation)
1481 if current_journal
1486 if current_journal
1482 current_journal.journalize_relation(relation, :removed)
1487 current_journal.journalize_relation(relation, :removed)
1483 current_journal.save
1488 current_journal.save
1484 end
1489 end
1485 end
1490 end
1486
1491
1487 # Default assignment based on category
1492 # Default assignment based on category
1488 def default_assign
1493 def default_assign
1489 if assigned_to.nil? && category && category.assigned_to
1494 if assigned_to.nil? && category && category.assigned_to
1490 self.assigned_to = category.assigned_to
1495 self.assigned_to = category.assigned_to
1491 end
1496 end
1492 end
1497 end
1493
1498
1494 # Updates start/due dates of following issues
1499 # Updates start/due dates of following issues
1495 def reschedule_following_issues
1500 def reschedule_following_issues
1496 if start_date_changed? || due_date_changed?
1501 if start_date_changed? || due_date_changed?
1497 relations_from.each do |relation|
1502 relations_from.each do |relation|
1498 relation.set_issue_to_dates
1503 relation.set_issue_to_dates
1499 end
1504 end
1500 end
1505 end
1501 end
1506 end
1502
1507
1503 # Closes duplicates if the issue is being closed
1508 # Closes duplicates if the issue is being closed
1504 def close_duplicates
1509 def close_duplicates
1505 if closing?
1510 if closing?
1506 duplicates.each do |duplicate|
1511 duplicates.each do |duplicate|
1507 # Reload is needed in case the duplicate was updated by a previous duplicate
1512 # Reload is needed in case the duplicate was updated by a previous duplicate
1508 duplicate.reload
1513 duplicate.reload
1509 # Don't re-close it if it's already closed
1514 # Don't re-close it if it's already closed
1510 next if duplicate.closed?
1515 next if duplicate.closed?
1511 # Same user and notes
1516 # Same user and notes
1512 if @current_journal
1517 if @current_journal
1513 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1518 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1514 end
1519 end
1515 duplicate.update_attribute :status, self.status
1520 duplicate.update_attribute :status, self.status
1516 end
1521 end
1517 end
1522 end
1518 end
1523 end
1519
1524
1520 # Make sure updated_on is updated when adding a note and set updated_on now
1525 # Make sure updated_on is updated when adding a note and set updated_on now
1521 # so we can set closed_on with the same value on closing
1526 # so we can set closed_on with the same value on closing
1522 def force_updated_on_change
1527 def force_updated_on_change
1523 if @current_journal || changed?
1528 if @current_journal || changed?
1524 self.updated_on = current_time_from_proper_timezone
1529 self.updated_on = current_time_from_proper_timezone
1525 if new_record?
1530 if new_record?
1526 self.created_on = updated_on
1531 self.created_on = updated_on
1527 end
1532 end
1528 end
1533 end
1529 end
1534 end
1530
1535
1531 # Callback for setting closed_on when the issue is closed.
1536 # Callback for setting closed_on when the issue is closed.
1532 # The closed_on attribute stores the time of the last closing
1537 # The closed_on attribute stores the time of the last closing
1533 # and is preserved when the issue is reopened.
1538 # and is preserved when the issue is reopened.
1534 def update_closed_on
1539 def update_closed_on
1535 if closing?
1540 if closing?
1536 self.closed_on = updated_on
1541 self.closed_on = updated_on
1537 end
1542 end
1538 end
1543 end
1539
1544
1540 # Saves the changes in a Journal
1545 # Saves the changes in a Journal
1541 # Called after_save
1546 # Called after_save
1542 def create_journal
1547 def create_journal
1543 if current_journal
1548 if current_journal
1544 current_journal.save
1549 current_journal.save
1545 end
1550 end
1546 end
1551 end
1547
1552
1548 def send_notification
1553 def send_notification
1549 if Setting.notified_events.include?('issue_added')
1554 if Setting.notified_events.include?('issue_added')
1550 Mailer.deliver_issue_add(self)
1555 Mailer.deliver_issue_add(self)
1551 end
1556 end
1552 end
1557 end
1553
1558
1554 # Stores the previous assignee so we can still have access
1559 # Stores the previous assignee so we can still have access
1555 # to it during after_save callbacks (assigned_to_id_was is reset)
1560 # to it during after_save callbacks (assigned_to_id_was is reset)
1556 def set_assigned_to_was
1561 def set_assigned_to_was
1557 @previous_assigned_to_id = assigned_to_id_was
1562 @previous_assigned_to_id = assigned_to_id_was
1558 end
1563 end
1559
1564
1560 # Clears the previous assignee at the end of after_save callbacks
1565 # Clears the previous assignee at the end of after_save callbacks
1561 def clear_assigned_to_was
1566 def clear_assigned_to_was
1562 @assigned_to_was = nil
1567 @assigned_to_was = nil
1563 @previous_assigned_to_id = nil
1568 @previous_assigned_to_id = nil
1564 end
1569 end
1565 end
1570 end
@@ -1,52 +1,52
1 <%= labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
1 <%= labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
2 <%= error_messages_for 'issue', 'time_entry' %>
2 <%= error_messages_for 'issue', 'time_entry' %>
3 <%= render :partial => 'conflict' if @conflict %>
3 <%= render :partial => 'conflict' if @conflict %>
4 <div class="box">
4 <div class="box">
5 <% if @edit_allowed %>
5 <% if @issue.attributes_editable? %>
6 <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
6 <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
7 <div id="all_attributes">
7 <div id="all_attributes">
8 <%= render :partial => 'form', :locals => {:f => f} %>
8 <%= render :partial => 'form', :locals => {:f => f} %>
9 </div>
9 </div>
10 </fieldset>
10 </fieldset>
11 <% end %>
11 <% end %>
12 <% if User.current.allowed_to?(:log_time, @project) %>
12 <% if User.current.allowed_to?(:log_time, @project) %>
13 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
13 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
14 <%= labelled_fields_for :time_entry, @time_entry do |time_entry| %>
14 <%= labelled_fields_for :time_entry, @time_entry do |time_entry| %>
15 <div class="splitcontentleft">
15 <div class="splitcontentleft">
16 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
16 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
17 </div>
17 </div>
18 <div class="splitcontentright">
18 <div class="splitcontentright">
19 <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
19 <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
20 </div>
20 </div>
21 <p><%= time_entry.text_field :comments, :size => 60 %></p>
21 <p><%= time_entry.text_field :comments, :size => 60 %></p>
22 <% @time_entry.custom_field_values.each do |value| %>
22 <% @time_entry.custom_field_values.each do |value| %>
23 <p><%= custom_field_tag_with_label :time_entry, value %></p>
23 <p><%= custom_field_tag_with_label :time_entry, value %></p>
24 <% end %>
24 <% end %>
25 <% end %>
25 <% end %>
26 </fieldset>
26 </fieldset>
27 <% end %>
27 <% end %>
28
28
29 <fieldset><legend><%= l(:field_notes) %></legend>
29 <fieldset><legend><%= l(:field_notes) %></legend>
30 <%= f.text_area :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', :no_label => true %>
30 <%= f.text_area :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', :no_label => true %>
31 <%= wikitoolbar_for 'issue_notes' %>
31 <%= wikitoolbar_for 'issue_notes' %>
32
32
33 <% if @issue.safe_attribute? 'private_notes' %>
33 <% if @issue.safe_attribute? 'private_notes' %>
34 <%= f.check_box :private_notes, :no_label => true %> <label for="issue_private_notes"><%= l(:field_private_notes) %></label>
34 <%= f.check_box :private_notes, :no_label => true %> <label for="issue_private_notes"><%= l(:field_private_notes) %></label>
35 <% end %>
35 <% end %>
36
36
37 <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
37 <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
38 </fieldset>
38 </fieldset>
39
39
40 <fieldset><legend><%= l(:label_attachment_plural) %></legend>
40 <fieldset><legend><%= l(:label_attachment_plural) %></legend>
41 <p><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
41 <p><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
42 </fieldset>
42 </fieldset>
43 </div>
43 </div>
44
44
45 <%= f.hidden_field :lock_version %>
45 <%= f.hidden_field :lock_version %>
46 <%= hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
46 <%= hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
47 <%= submit_tag l(:button_submit) %>
47 <%= submit_tag l(:button_submit) %>
48 <%= preview_link preview_edit_issue_path(:project_id => @project, :id => @issue), 'issue-form' %>
48 <%= preview_link preview_edit_issue_path(:project_id => @project, :id => @issue), 'issue-form' %>
49 | <%= link_to l(:button_cancel), {}, :onclick => "$('#update').hide(); return false;" %>
49 | <%= link_to l(:button_cancel), {}, :onclick => "$('#update').hide(); return false;" %>
50 <% end %>
50 <% end %>
51
51
52 <div id="preview" class="wiki"></div>
52 <div id="preview" class="wiki"></div>
General Comments 0
You need to be logged in to leave comments. Login now