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