##// END OF EJS Templates
Don't join all associations by default (#24865)....
Jean-Philippe Lang -
r15839:aa354e627516
parent child
Show More
@@ -1,569 +1,568
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 default_search_scope :issues
19 default_search_scope :issues
20
20
21 before_action :find_issue, :only => [:show, :edit, :update]
21 before_action :find_issue, :only => [:show, :edit, :update]
22 before_action :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
22 before_action :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_action :authorize, :except => [:index, :new, :create]
23 before_action :authorize, :except => [:index, :new, :create]
24 before_action :find_optional_project, :only => [:index, :new, :create]
24 before_action :find_optional_project, :only => [:index, :new, :create]
25 before_action :build_new_issue_from_params, :only => [:new, :create]
25 before_action :build_new_issue_from_params, :only => [:new, :create]
26 accept_rss_auth :index, :show
26 accept_rss_auth :index, :show
27 accept_api_auth :index, :show, :create, :update, :destroy
27 accept_api_auth :index, :show, :create, :update, :destroy
28
28
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30
30
31 helper :journals
31 helper :journals
32 helper :projects
32 helper :projects
33 helper :custom_fields
33 helper :custom_fields
34 helper :issue_relations
34 helper :issue_relations
35 helper :watchers
35 helper :watchers
36 helper :attachments
36 helper :attachments
37 helper :queries
37 helper :queries
38 include QueriesHelper
38 include QueriesHelper
39 helper :repositories
39 helper :repositories
40 helper :sort
40 helper :sort
41 include SortHelper
41 include SortHelper
42 helper :timelog
42 helper :timelog
43
43
44 def index
44 def index
45 retrieve_query
45 retrieve_query
46 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
46 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
47 sort_update(@query.sortable_columns)
47 sort_update(@query.sortable_columns)
48 @query.sort_criteria = sort_criteria.to_a
48 @query.sort_criteria = sort_criteria.to_a
49
49
50 if @query.valid?
50 if @query.valid?
51 case params[:format]
51 case params[:format]
52 when 'csv', 'pdf'
52 when 'csv', 'pdf'
53 @limit = Setting.issues_export_limit.to_i
53 @limit = Setting.issues_export_limit.to_i
54 if params[:columns] == 'all'
54 if params[:columns] == 'all'
55 @query.column_names = @query.available_inline_columns.map(&:name)
55 @query.column_names = @query.available_inline_columns.map(&:name)
56 end
56 end
57 when 'atom'
57 when 'atom'
58 @limit = Setting.feeds_limit.to_i
58 @limit = Setting.feeds_limit.to_i
59 when 'xml', 'json'
59 when 'xml', 'json'
60 @offset, @limit = api_offset_and_limit
60 @offset, @limit = api_offset_and_limit
61 @query.column_names = %w(author)
61 @query.column_names = %w(author)
62 else
62 else
63 @limit = per_page_option
63 @limit = per_page_option
64 end
64 end
65
65
66 @issue_count = @query.issue_count
66 @issue_count = @query.issue_count
67 @issue_pages = Paginator.new @issue_count, @limit, params['page']
67 @issue_pages = Paginator.new @issue_count, @limit, params['page']
68 @offset ||= @issue_pages.offset
68 @offset ||= @issue_pages.offset
69 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
69 @issues = @query.issues(:order => sort_clause,
70 :order => sort_clause,
71 :offset => @offset,
70 :offset => @offset,
72 :limit => @limit)
71 :limit => @limit)
73 @issue_count_by_group = @query.issue_count_by_group
72 @issue_count_by_group = @query.issue_count_by_group
74
73
75 respond_to do |format|
74 respond_to do |format|
76 format.html { render :template => 'issues/index', :layout => !request.xhr? }
75 format.html { render :template => 'issues/index', :layout => !request.xhr? }
77 format.api {
76 format.api {
78 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
77 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
79 }
78 }
80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
79 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
81 format.csv { send_data(query_to_csv(@issues, @query, params[:csv]), :type => 'text/csv; header=present', :filename => 'issues.csv') }
80 format.csv { send_data(query_to_csv(@issues, @query, params[:csv]), :type => 'text/csv; header=present', :filename => 'issues.csv') }
82 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
81 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
83 end
82 end
84 else
83 else
85 respond_to do |format|
84 respond_to do |format|
86 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
85 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
87 format.any(:atom, :csv, :pdf) { head 422 }
86 format.any(:atom, :csv, :pdf) { head 422 }
88 format.api { render_validation_errors(@query) }
87 format.api { render_validation_errors(@query) }
89 end
88 end
90 end
89 end
91 rescue ActiveRecord::RecordNotFound
90 rescue ActiveRecord::RecordNotFound
92 render_404
91 render_404
93 end
92 end
94
93
95 def show
94 def show
96 @journals = @issue.visible_journals_with_index
95 @journals = @issue.visible_journals_with_index
97 @changesets = @issue.changesets.visible.preload(:repository, :user).to_a
96 @changesets = @issue.changesets.visible.preload(:repository, :user).to_a
98 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
97 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
99
98
100 if User.current.wants_comments_in_reverse_order?
99 if User.current.wants_comments_in_reverse_order?
101 @journals.reverse!
100 @journals.reverse!
102 @changesets.reverse!
101 @changesets.reverse!
103 end
102 end
104
103
105 respond_to do |format|
104 respond_to do |format|
106 format.html {
105 format.html {
107 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
106 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
108 @priorities = IssuePriority.active
107 @priorities = IssuePriority.active
109 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
108 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
110 @relation = IssueRelation.new
109 @relation = IssueRelation.new
111 retrieve_previous_and_next_issue_ids
110 retrieve_previous_and_next_issue_ids
112 render :template => 'issues/show'
111 render :template => 'issues/show'
113 }
112 }
114 format.api
113 format.api
115 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
114 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
116 format.pdf {
115 format.pdf {
117 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
116 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
118 }
117 }
119 end
118 end
120 end
119 end
121
120
122 def new
121 def new
123 respond_to do |format|
122 respond_to do |format|
124 format.html { render :action => 'new', :layout => !request.xhr? }
123 format.html { render :action => 'new', :layout => !request.xhr? }
125 format.js
124 format.js
126 end
125 end
127 end
126 end
128
127
129 def create
128 def create
130 unless User.current.allowed_to?(:add_issues, @issue.project, :global => true)
129 unless User.current.allowed_to?(:add_issues, @issue.project, :global => true)
131 raise ::Unauthorized
130 raise ::Unauthorized
132 end
131 end
133 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
132 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
134 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
133 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
135 if @issue.save
134 if @issue.save
136 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
135 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
137 respond_to do |format|
136 respond_to do |format|
138 format.html {
137 format.html {
139 render_attachment_warning_if_needed(@issue)
138 render_attachment_warning_if_needed(@issue)
140 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
139 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
141 redirect_after_create
140 redirect_after_create
142 }
141 }
143 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
142 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
144 end
143 end
145 return
144 return
146 else
145 else
147 respond_to do |format|
146 respond_to do |format|
148 format.html {
147 format.html {
149 if @issue.project.nil?
148 if @issue.project.nil?
150 render_error :status => 422
149 render_error :status => 422
151 else
150 else
152 render :action => 'new'
151 render :action => 'new'
153 end
152 end
154 }
153 }
155 format.api { render_validation_errors(@issue) }
154 format.api { render_validation_errors(@issue) }
156 end
155 end
157 end
156 end
158 end
157 end
159
158
160 def edit
159 def edit
161 return unless update_issue_from_params
160 return unless update_issue_from_params
162
161
163 respond_to do |format|
162 respond_to do |format|
164 format.html { }
163 format.html { }
165 format.js
164 format.js
166 end
165 end
167 end
166 end
168
167
169 def update
168 def update
170 return unless update_issue_from_params
169 return unless update_issue_from_params
171 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
170 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
172 saved = false
171 saved = false
173 begin
172 begin
174 saved = save_issue_with_child_records
173 saved = save_issue_with_child_records
175 rescue ActiveRecord::StaleObjectError
174 rescue ActiveRecord::StaleObjectError
176 @conflict = true
175 @conflict = true
177 if params[:last_journal_id]
176 if params[:last_journal_id]
178 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
177 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
179 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
178 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
180 end
179 end
181 end
180 end
182
181
183 if saved
182 if saved
184 render_attachment_warning_if_needed(@issue)
183 render_attachment_warning_if_needed(@issue)
185 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
184 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
186
185
187 respond_to do |format|
186 respond_to do |format|
188 format.html { redirect_back_or_default issue_path(@issue, previous_and_next_issue_ids_params) }
187 format.html { redirect_back_or_default issue_path(@issue, previous_and_next_issue_ids_params) }
189 format.api { render_api_ok }
188 format.api { render_api_ok }
190 end
189 end
191 else
190 else
192 respond_to do |format|
191 respond_to do |format|
193 format.html { render :action => 'edit' }
192 format.html { render :action => 'edit' }
194 format.api { render_validation_errors(@issue) }
193 format.api { render_validation_errors(@issue) }
195 end
194 end
196 end
195 end
197 end
196 end
198
197
199 # Bulk edit/copy a set of issues
198 # Bulk edit/copy a set of issues
200 def bulk_edit
199 def bulk_edit
201 @issues.sort!
200 @issues.sort!
202 @copy = params[:copy].present?
201 @copy = params[:copy].present?
203 @notes = params[:notes]
202 @notes = params[:notes]
204
203
205 if @copy
204 if @copy
206 unless User.current.allowed_to?(:copy_issues, @projects)
205 unless User.current.allowed_to?(:copy_issues, @projects)
207 raise ::Unauthorized
206 raise ::Unauthorized
208 end
207 end
209 else
208 else
210 unless @issues.all?(&:attributes_editable?)
209 unless @issues.all?(&:attributes_editable?)
211 raise ::Unauthorized
210 raise ::Unauthorized
212 end
211 end
213 end
212 end
214
213
215 edited_issues = Issue.where(:id => @issues.map(&:id)).to_a
214 edited_issues = Issue.where(:id => @issues.map(&:id)).to_a
216
215
217 @allowed_projects = Issue.allowed_target_projects
216 @allowed_projects = Issue.allowed_target_projects
218 if params[:issue]
217 if params[:issue]
219 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
218 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
220 if @target_project
219 if @target_project
221 target_projects = [@target_project]
220 target_projects = [@target_project]
222 edited_issues.each {|issue| issue.project = @target_project}
221 edited_issues.each {|issue| issue.project = @target_project}
223 end
222 end
224 end
223 end
225 target_projects ||= @projects
224 target_projects ||= @projects
226
225
227 @trackers = target_projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
226 @trackers = target_projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
228 if params[:issue]
227 if params[:issue]
229 @target_tracker = @trackers.detect {|t| t.id.to_s == params[:issue][:tracker_id].to_s}
228 @target_tracker = @trackers.detect {|t| t.id.to_s == params[:issue][:tracker_id].to_s}
230 if @target_tracker
229 if @target_tracker
231 edited_issues.each {|issue| issue.tracker = @target_tracker}
230 edited_issues.each {|issue| issue.tracker = @target_tracker}
232 end
231 end
233 end
232 end
234
233
235 if @copy
234 if @copy
236 # Copied issues will get their default statuses
235 # Copied issues will get their default statuses
237 @available_statuses = []
236 @available_statuses = []
238 else
237 else
239 @available_statuses = edited_issues.map(&:new_statuses_allowed_to).reduce(:&)
238 @available_statuses = edited_issues.map(&:new_statuses_allowed_to).reduce(:&)
240 end
239 end
241 if params[:issue]
240 if params[:issue]
242 @target_status = @available_statuses.detect {|t| t.id.to_s == params[:issue][:status_id].to_s}
241 @target_status = @available_statuses.detect {|t| t.id.to_s == params[:issue][:status_id].to_s}
243 if @target_status
242 if @target_status
244 edited_issues.each {|issue| issue.status = @target_status}
243 edited_issues.each {|issue| issue.status = @target_status}
245 end
244 end
246 end
245 end
247
246
248 @custom_fields = edited_issues.map{|i|i.editable_custom_fields}.reduce(:&).select {|field| field.format.bulk_edit_supported}
247 @custom_fields = edited_issues.map{|i|i.editable_custom_fields}.reduce(:&).select {|field| field.format.bulk_edit_supported}
249 @assignables = target_projects.map(&:assignable_users).reduce(:&)
248 @assignables = target_projects.map(&:assignable_users).reduce(:&)
250 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
249 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
251 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
250 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
252 if @copy
251 if @copy
253 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
252 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
254 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
253 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
255 end
254 end
256
255
257 @safe_attributes = edited_issues.map(&:safe_attribute_names).reduce(:&)
256 @safe_attributes = edited_issues.map(&:safe_attribute_names).reduce(:&)
258
257
259 @issue_params = params[:issue] || {}
258 @issue_params = params[:issue] || {}
260 @issue_params[:custom_field_values] ||= {}
259 @issue_params[:custom_field_values] ||= {}
261 end
260 end
262
261
263 def bulk_update
262 def bulk_update
264 @issues.sort!
263 @issues.sort!
265 @copy = params[:copy].present?
264 @copy = params[:copy].present?
266
265
267 attributes = parse_params_for_bulk_update(params[:issue])
266 attributes = parse_params_for_bulk_update(params[:issue])
268 copy_subtasks = (params[:copy_subtasks] == '1')
267 copy_subtasks = (params[:copy_subtasks] == '1')
269 copy_attachments = (params[:copy_attachments] == '1')
268 copy_attachments = (params[:copy_attachments] == '1')
270
269
271 if @copy
270 if @copy
272 unless User.current.allowed_to?(:copy_issues, @projects)
271 unless User.current.allowed_to?(:copy_issues, @projects)
273 raise ::Unauthorized
272 raise ::Unauthorized
274 end
273 end
275 target_projects = @projects
274 target_projects = @projects
276 if attributes['project_id'].present?
275 if attributes['project_id'].present?
277 target_projects = Project.where(:id => attributes['project_id']).to_a
276 target_projects = Project.where(:id => attributes['project_id']).to_a
278 end
277 end
279 unless User.current.allowed_to?(:add_issues, target_projects)
278 unless User.current.allowed_to?(:add_issues, target_projects)
280 raise ::Unauthorized
279 raise ::Unauthorized
281 end
280 end
282 else
281 else
283 unless @issues.all?(&:attributes_editable?)
282 unless @issues.all?(&:attributes_editable?)
284 raise ::Unauthorized
283 raise ::Unauthorized
285 end
284 end
286 end
285 end
287
286
288 unsaved_issues = []
287 unsaved_issues = []
289 saved_issues = []
288 saved_issues = []
290
289
291 if @copy && copy_subtasks
290 if @copy && copy_subtasks
292 # Descendant issues will be copied with the parent task
291 # Descendant issues will be copied with the parent task
293 # Don't copy them twice
292 # Don't copy them twice
294 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
293 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
295 end
294 end
296
295
297 @issues.each do |orig_issue|
296 @issues.each do |orig_issue|
298 orig_issue.reload
297 orig_issue.reload
299 if @copy
298 if @copy
300 issue = orig_issue.copy({},
299 issue = orig_issue.copy({},
301 :attachments => copy_attachments,
300 :attachments => copy_attachments,
302 :subtasks => copy_subtasks,
301 :subtasks => copy_subtasks,
303 :link => link_copy?(params[:link_copy])
302 :link => link_copy?(params[:link_copy])
304 )
303 )
305 else
304 else
306 issue = orig_issue
305 issue = orig_issue
307 end
306 end
308 journal = issue.init_journal(User.current, params[:notes])
307 journal = issue.init_journal(User.current, params[:notes])
309 issue.safe_attributes = attributes
308 issue.safe_attributes = attributes
310 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
309 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
311 if issue.save
310 if issue.save
312 saved_issues << issue
311 saved_issues << issue
313 else
312 else
314 unsaved_issues << orig_issue
313 unsaved_issues << orig_issue
315 end
314 end
316 end
315 end
317
316
318 if unsaved_issues.empty?
317 if unsaved_issues.empty?
319 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
318 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
320 if params[:follow]
319 if params[:follow]
321 if @issues.size == 1 && saved_issues.size == 1
320 if @issues.size == 1 && saved_issues.size == 1
322 redirect_to issue_path(saved_issues.first)
321 redirect_to issue_path(saved_issues.first)
323 elsif saved_issues.map(&:project).uniq.size == 1
322 elsif saved_issues.map(&:project).uniq.size == 1
324 redirect_to project_issues_path(saved_issues.map(&:project).first)
323 redirect_to project_issues_path(saved_issues.map(&:project).first)
325 end
324 end
326 else
325 else
327 redirect_back_or_default _project_issues_path(@project)
326 redirect_back_or_default _project_issues_path(@project)
328 end
327 end
329 else
328 else
330 @saved_issues = @issues
329 @saved_issues = @issues
331 @unsaved_issues = unsaved_issues
330 @unsaved_issues = unsaved_issues
332 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
331 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
333 bulk_edit
332 bulk_edit
334 render :action => 'bulk_edit'
333 render :action => 'bulk_edit'
335 end
334 end
336 end
335 end
337
336
338 def destroy
337 def destroy
339 raise Unauthorized unless @issues.all?(&:deletable?)
338 raise Unauthorized unless @issues.all?(&:deletable?)
340
339
341 # all issues and their descendants are about to be deleted
340 # all issues and their descendants are about to be deleted
342 issues_and_descendants_ids = Issue.self_and_descendants(@issues).pluck(:id)
341 issues_and_descendants_ids = Issue.self_and_descendants(@issues).pluck(:id)
343 time_entries = TimeEntry.where(:issue_id => issues_and_descendants_ids)
342 time_entries = TimeEntry.where(:issue_id => issues_and_descendants_ids)
344 @hours = time_entries.sum(:hours).to_f
343 @hours = time_entries.sum(:hours).to_f
345
344
346 if @hours > 0
345 if @hours > 0
347 case params[:todo]
346 case params[:todo]
348 when 'destroy'
347 when 'destroy'
349 # nothing to do
348 # nothing to do
350 when 'nullify'
349 when 'nullify'
351 time_entries.update_all(:issue_id => nil)
350 time_entries.update_all(:issue_id => nil)
352 when 'reassign'
351 when 'reassign'
353 reassign_to = @project && @project.issues.find_by_id(params[:reassign_to_id])
352 reassign_to = @project && @project.issues.find_by_id(params[:reassign_to_id])
354 if reassign_to.nil?
353 if reassign_to.nil?
355 flash.now[:error] = l(:error_issue_not_found_in_project)
354 flash.now[:error] = l(:error_issue_not_found_in_project)
356 return
355 return
357 elsif issues_and_descendants_ids.include?(reassign_to.id)
356 elsif issues_and_descendants_ids.include?(reassign_to.id)
358 flash.now[:error] = l(:error_cannot_reassign_time_entries_to_an_issue_about_to_be_deleted)
357 flash.now[:error] = l(:error_cannot_reassign_time_entries_to_an_issue_about_to_be_deleted)
359 return
358 return
360 else
359 else
361 time_entries.update_all(:issue_id => reassign_to.id, :project_id => reassign_to.project_id)
360 time_entries.update_all(:issue_id => reassign_to.id, :project_id => reassign_to.project_id)
362 end
361 end
363 else
362 else
364 # display the destroy form if it's a user request
363 # display the destroy form if it's a user request
365 return unless api_request?
364 return unless api_request?
366 end
365 end
367 end
366 end
368 @issues.each do |issue|
367 @issues.each do |issue|
369 begin
368 begin
370 issue.reload.destroy
369 issue.reload.destroy
371 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
370 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
372 # nothing to do, issue was already deleted (eg. by a parent)
371 # nothing to do, issue was already deleted (eg. by a parent)
373 end
372 end
374 end
373 end
375 respond_to do |format|
374 respond_to do |format|
376 format.html { redirect_back_or_default _project_issues_path(@project) }
375 format.html { redirect_back_or_default _project_issues_path(@project) }
377 format.api { render_api_ok }
376 format.api { render_api_ok }
378 end
377 end
379 end
378 end
380
379
381 # Overrides Redmine::MenuManager::MenuController::ClassMethods for
380 # Overrides Redmine::MenuManager::MenuController::ClassMethods for
382 # when the "New issue" tab is enabled
381 # when the "New issue" tab is enabled
383 def current_menu_item
382 def current_menu_item
384 if Setting.new_item_menu_tab == '1' && [:new, :create].include?(action_name.to_sym)
383 if Setting.new_item_menu_tab == '1' && [:new, :create].include?(action_name.to_sym)
385 :new_issue
384 :new_issue
386 else
385 else
387 super
386 super
388 end
387 end
389 end
388 end
390
389
391 private
390 private
392
391
393 def retrieve_previous_and_next_issue_ids
392 def retrieve_previous_and_next_issue_ids
394 if params[:prev_issue_id].present? || params[:next_issue_id].present?
393 if params[:prev_issue_id].present? || params[:next_issue_id].present?
395 @prev_issue_id = params[:prev_issue_id].presence.try(:to_i)
394 @prev_issue_id = params[:prev_issue_id].presence.try(:to_i)
396 @next_issue_id = params[:next_issue_id].presence.try(:to_i)
395 @next_issue_id = params[:next_issue_id].presence.try(:to_i)
397 @issue_position = params[:issue_position].presence.try(:to_i)
396 @issue_position = params[:issue_position].presence.try(:to_i)
398 @issue_count = params[:issue_count].presence.try(:to_i)
397 @issue_count = params[:issue_count].presence.try(:to_i)
399 else
398 else
400 retrieve_query_from_session
399 retrieve_query_from_session
401 if @query
400 if @query
402 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
401 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
403 sort_update(@query.sortable_columns, 'issues_index_sort')
402 sort_update(@query.sortable_columns, 'issues_index_sort')
404 limit = 500
403 limit = 500
405 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
404 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1))
406 if (idx = issue_ids.index(@issue.id)) && idx < limit
405 if (idx = issue_ids.index(@issue.id)) && idx < limit
407 if issue_ids.size < 500
406 if issue_ids.size < 500
408 @issue_position = idx + 1
407 @issue_position = idx + 1
409 @issue_count = issue_ids.size
408 @issue_count = issue_ids.size
410 end
409 end
411 @prev_issue_id = issue_ids[idx - 1] if idx > 0
410 @prev_issue_id = issue_ids[idx - 1] if idx > 0
412 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
411 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
413 end
412 end
414 end
413 end
415 end
414 end
416 end
415 end
417
416
418 def previous_and_next_issue_ids_params
417 def previous_and_next_issue_ids_params
419 {
418 {
420 :prev_issue_id => params[:prev_issue_id],
419 :prev_issue_id => params[:prev_issue_id],
421 :next_issue_id => params[:next_issue_id],
420 :next_issue_id => params[:next_issue_id],
422 :issue_position => params[:issue_position],
421 :issue_position => params[:issue_position],
423 :issue_count => params[:issue_count]
422 :issue_count => params[:issue_count]
424 }.reject {|k,v| k.blank?}
423 }.reject {|k,v| k.blank?}
425 end
424 end
426
425
427 # Used by #edit and #update to set some common instance variables
426 # Used by #edit and #update to set some common instance variables
428 # from the params
427 # from the params
429 def update_issue_from_params
428 def update_issue_from_params
430 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
429 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
431 if params[:time_entry]
430 if params[:time_entry]
432 @time_entry.safe_attributes = params[:time_entry]
431 @time_entry.safe_attributes = params[:time_entry]
433 end
432 end
434
433
435 @issue.init_journal(User.current)
434 @issue.init_journal(User.current)
436
435
437 issue_attributes = params[:issue]
436 issue_attributes = params[:issue]
438 if issue_attributes && params[:conflict_resolution]
437 if issue_attributes && params[:conflict_resolution]
439 case params[:conflict_resolution]
438 case params[:conflict_resolution]
440 when 'overwrite'
439 when 'overwrite'
441 issue_attributes = issue_attributes.dup
440 issue_attributes = issue_attributes.dup
442 issue_attributes.delete(:lock_version)
441 issue_attributes.delete(:lock_version)
443 when 'add_notes'
442 when 'add_notes'
444 issue_attributes = issue_attributes.slice(:notes, :private_notes)
443 issue_attributes = issue_attributes.slice(:notes, :private_notes)
445 when 'cancel'
444 when 'cancel'
446 redirect_to issue_path(@issue)
445 redirect_to issue_path(@issue)
447 return false
446 return false
448 end
447 end
449 end
448 end
450 @issue.safe_attributes = issue_attributes
449 @issue.safe_attributes = issue_attributes
451 @priorities = IssuePriority.active
450 @priorities = IssuePriority.active
452 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
451 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
453 true
452 true
454 end
453 end
455
454
456 # Used by #new and #create to build a new issue from the params
455 # Used by #new and #create to build a new issue from the params
457 # The new issue will be copied from an existing one if copy_from parameter is given
456 # The new issue will be copied from an existing one if copy_from parameter is given
458 def build_new_issue_from_params
457 def build_new_issue_from_params
459 @issue = Issue.new
458 @issue = Issue.new
460 if params[:copy_from]
459 if params[:copy_from]
461 begin
460 begin
462 @issue.init_journal(User.current)
461 @issue.init_journal(User.current)
463 @copy_from = Issue.visible.find(params[:copy_from])
462 @copy_from = Issue.visible.find(params[:copy_from])
464 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
463 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
465 raise ::Unauthorized
464 raise ::Unauthorized
466 end
465 end
467 @link_copy = link_copy?(params[:link_copy]) || request.get?
466 @link_copy = link_copy?(params[:link_copy]) || request.get?
468 @copy_attachments = params[:copy_attachments].present? || request.get?
467 @copy_attachments = params[:copy_attachments].present? || request.get?
469 @copy_subtasks = params[:copy_subtasks].present? || request.get?
468 @copy_subtasks = params[:copy_subtasks].present? || request.get?
470 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
469 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
471 @issue.parent_issue_id = @copy_from.parent_id
470 @issue.parent_issue_id = @copy_from.parent_id
472 rescue ActiveRecord::RecordNotFound
471 rescue ActiveRecord::RecordNotFound
473 render_404
472 render_404
474 return
473 return
475 end
474 end
476 end
475 end
477 @issue.project = @project
476 @issue.project = @project
478 if request.get?
477 if request.get?
479 @issue.project ||= @issue.allowed_target_projects.first
478 @issue.project ||= @issue.allowed_target_projects.first
480 end
479 end
481 @issue.author ||= User.current
480 @issue.author ||= User.current
482 @issue.start_date ||= User.current.today if Setting.default_issue_start_date_to_creation_date?
481 @issue.start_date ||= User.current.today if Setting.default_issue_start_date_to_creation_date?
483
482
484 attrs = (params[:issue] || {}).deep_dup
483 attrs = (params[:issue] || {}).deep_dup
485 if action_name == 'new' && params[:was_default_status] == attrs[:status_id]
484 if action_name == 'new' && params[:was_default_status] == attrs[:status_id]
486 attrs.delete(:status_id)
485 attrs.delete(:status_id)
487 end
486 end
488 if action_name == 'new' && params[:form_update_triggered_by] == 'issue_project_id'
487 if action_name == 'new' && params[:form_update_triggered_by] == 'issue_project_id'
489 # Discard submitted version when changing the project on the issue form
488 # Discard submitted version when changing the project on the issue form
490 # so we can use the default version for the new project
489 # so we can use the default version for the new project
491 attrs.delete(:fixed_version_id)
490 attrs.delete(:fixed_version_id)
492 end
491 end
493 @issue.safe_attributes = attrs
492 @issue.safe_attributes = attrs
494
493
495 if @issue.project
494 if @issue.project
496 @issue.tracker ||= @issue.allowed_target_trackers.first
495 @issue.tracker ||= @issue.allowed_target_trackers.first
497 if @issue.tracker.nil?
496 if @issue.tracker.nil?
498 if @issue.project.trackers.any?
497 if @issue.project.trackers.any?
499 # None of the project trackers is allowed to the user
498 # None of the project trackers is allowed to the user
500 render_error :message => l(:error_no_tracker_allowed_for_new_issue_in_project), :status => 403
499 render_error :message => l(:error_no_tracker_allowed_for_new_issue_in_project), :status => 403
501 else
500 else
502 # Project has no trackers
501 # Project has no trackers
503 render_error l(:error_no_tracker_in_project)
502 render_error l(:error_no_tracker_in_project)
504 end
503 end
505 return false
504 return false
506 end
505 end
507 if @issue.status.nil?
506 if @issue.status.nil?
508 render_error l(:error_no_default_issue_status)
507 render_error l(:error_no_default_issue_status)
509 return false
508 return false
510 end
509 end
511 elsif request.get?
510 elsif request.get?
512 render_error :message => l(:error_no_projects_with_tracker_allowed_for_new_issue), :status => 403
511 render_error :message => l(:error_no_projects_with_tracker_allowed_for_new_issue), :status => 403
513 return false
512 return false
514 end
513 end
515
514
516 @priorities = IssuePriority.active
515 @priorities = IssuePriority.active
517 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
516 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
518 end
517 end
519
518
520 # Saves @issue and a time_entry from the parameters
519 # Saves @issue and a time_entry from the parameters
521 def save_issue_with_child_records
520 def save_issue_with_child_records
522 Issue.transaction do
521 Issue.transaction do
523 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
522 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
524 time_entry = @time_entry || TimeEntry.new
523 time_entry = @time_entry || TimeEntry.new
525 time_entry.project = @issue.project
524 time_entry.project = @issue.project
526 time_entry.issue = @issue
525 time_entry.issue = @issue
527 time_entry.user = User.current
526 time_entry.user = User.current
528 time_entry.spent_on = User.current.today
527 time_entry.spent_on = User.current.today
529 time_entry.attributes = params[:time_entry]
528 time_entry.attributes = params[:time_entry]
530 @issue.time_entries << time_entry
529 @issue.time_entries << time_entry
531 end
530 end
532
531
533 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
532 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
534 if @issue.save
533 if @issue.save
535 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
534 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
536 else
535 else
537 raise ActiveRecord::Rollback
536 raise ActiveRecord::Rollback
538 end
537 end
539 end
538 end
540 end
539 end
541
540
542 # Returns true if the issue copy should be linked
541 # Returns true if the issue copy should be linked
543 # to the original issue
542 # to the original issue
544 def link_copy?(param)
543 def link_copy?(param)
545 case Setting.link_copied_issue
544 case Setting.link_copied_issue
546 when 'yes'
545 when 'yes'
547 true
546 true
548 when 'no'
547 when 'no'
549 false
548 false
550 when 'ask'
549 when 'ask'
551 param == '1'
550 param == '1'
552 end
551 end
553 end
552 end
554
553
555 # Redirects user after a successful issue creation
554 # Redirects user after a successful issue creation
556 def redirect_after_create
555 def redirect_after_create
557 if params[:continue]
556 if params[:continue]
558 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
557 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
559 if params[:project_id]
558 if params[:project_id]
560 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
559 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
561 else
560 else
562 attrs.merge! :project_id => @issue.project_id
561 attrs.merge! :project_id => @issue.project_id
563 redirect_to new_issue_path(:issue => attrs)
562 redirect_to new_issue_path(:issue => attrs)
564 end
563 end
565 else
564 else
566 redirect_to issue_path(@issue)
565 redirect_to issue_path(@issue)
567 end
566 end
568 end
567 end
569 end
568 end
@@ -1,520 +1,535
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 IssueQuery < Query
18 class IssueQuery < Query
19
19
20 self.queried_class = Issue
20 self.queried_class = Issue
21 self.view_permission = :view_issues
21 self.view_permission = :view_issues
22
22
23 self.available_columns = [
23 self.available_columns = [
24 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
24 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
25 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
26 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
26 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
27 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
27 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
28 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
28 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
29 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
29 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
30 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
30 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
31 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
31 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
32 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
32 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
33 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
33 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
34 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
34 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
35 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
35 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
36 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
36 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
37 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
37 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
38 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true),
38 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true),
39 QueryColumn.new(:total_estimated_hours,
39 QueryColumn.new(:total_estimated_hours,
40 :sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
40 :sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
41 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
41 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
42 :default_order => 'desc'),
42 :default_order => 'desc'),
43 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
43 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
44 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
44 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
45 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
45 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
46 QueryColumn.new(:relations, :caption => :label_related_issues),
46 QueryColumn.new(:relations, :caption => :label_related_issues),
47 QueryColumn.new(:description, :inline => false)
47 QueryColumn.new(:description, :inline => false)
48 ]
48 ]
49
49
50 def initialize(attributes=nil, *args)
50 def initialize(attributes=nil, *args)
51 super attributes
51 super attributes
52 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
52 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
53 end
53 end
54
54
55 def draw_relations
55 def draw_relations
56 r = options[:draw_relations]
56 r = options[:draw_relations]
57 r.nil? || r == '1'
57 r.nil? || r == '1'
58 end
58 end
59
59
60 def draw_relations=(arg)
60 def draw_relations=(arg)
61 options[:draw_relations] = (arg == '0' ? '0' : nil)
61 options[:draw_relations] = (arg == '0' ? '0' : nil)
62 end
62 end
63
63
64 def draw_progress_line
64 def draw_progress_line
65 r = options[:draw_progress_line]
65 r = options[:draw_progress_line]
66 r == '1'
66 r == '1'
67 end
67 end
68
68
69 def draw_progress_line=(arg)
69 def draw_progress_line=(arg)
70 options[:draw_progress_line] = (arg == '1' ? '1' : nil)
70 options[:draw_progress_line] = (arg == '1' ? '1' : nil)
71 end
71 end
72
72
73 def build_from_params(params)
73 def build_from_params(params)
74 super
74 super
75 self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
75 self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
76 self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
76 self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
77 self
77 self
78 end
78 end
79
79
80 def initialize_available_filters
80 def initialize_available_filters
81 add_available_filter "status_id",
81 add_available_filter "status_id",
82 :type => :list_status, :values => lambda { IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] } }
82 :type => :list_status, :values => lambda { IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] } }
83
83
84 add_available_filter("project_id",
84 add_available_filter("project_id",
85 :type => :list, :values => lambda { project_values }
85 :type => :list, :values => lambda { project_values }
86 ) if project.nil?
86 ) if project.nil?
87
87
88 add_available_filter "tracker_id",
88 add_available_filter "tracker_id",
89 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
89 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
90
90
91 add_available_filter "priority_id",
91 add_available_filter "priority_id",
92 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
92 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
93
93
94 add_available_filter("author_id",
94 add_available_filter("author_id",
95 :type => :list, :values => lambda { author_values }
95 :type => :list, :values => lambda { author_values }
96 )
96 )
97
97
98 add_available_filter("assigned_to_id",
98 add_available_filter("assigned_to_id",
99 :type => :list_optional, :values => lambda { assigned_to_values }
99 :type => :list_optional, :values => lambda { assigned_to_values }
100 )
100 )
101
101
102 add_available_filter("member_of_group",
102 add_available_filter("member_of_group",
103 :type => :list_optional, :values => lambda { Group.givable.visible.collect {|g| [g.name, g.id.to_s] } }
103 :type => :list_optional, :values => lambda { Group.givable.visible.collect {|g| [g.name, g.id.to_s] } }
104 )
104 )
105
105
106 add_available_filter("assigned_to_role",
106 add_available_filter("assigned_to_role",
107 :type => :list_optional, :values => lambda { Role.givable.collect {|r| [r.name, r.id.to_s] } }
107 :type => :list_optional, :values => lambda { Role.givable.collect {|r| [r.name, r.id.to_s] } }
108 )
108 )
109
109
110 add_available_filter "fixed_version_id",
110 add_available_filter "fixed_version_id",
111 :type => :list_optional, :values => lambda { fixed_version_values }
111 :type => :list_optional, :values => lambda { fixed_version_values }
112
112
113 add_available_filter "fixed_version.due_date",
113 add_available_filter "fixed_version.due_date",
114 :type => :date,
114 :type => :date,
115 :name => l(:label_attribute_of_fixed_version, :name => l(:field_effective_date))
115 :name => l(:label_attribute_of_fixed_version, :name => l(:field_effective_date))
116
116
117 add_available_filter "fixed_version.status",
117 add_available_filter "fixed_version.status",
118 :type => :list,
118 :type => :list,
119 :name => l(:label_attribute_of_fixed_version, :name => l(:field_status)),
119 :name => l(:label_attribute_of_fixed_version, :name => l(:field_status)),
120 :values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s] }
120 :values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s] }
121
121
122 add_available_filter "category_id",
122 add_available_filter "category_id",
123 :type => :list_optional,
123 :type => :list_optional,
124 :values => lambda { project.issue_categories.collect{|s| [s.name, s.id.to_s] } } if project
124 :values => lambda { project.issue_categories.collect{|s| [s.name, s.id.to_s] } } if project
125
125
126 add_available_filter "subject", :type => :text
126 add_available_filter "subject", :type => :text
127 add_available_filter "description", :type => :text
127 add_available_filter "description", :type => :text
128 add_available_filter "created_on", :type => :date_past
128 add_available_filter "created_on", :type => :date_past
129 add_available_filter "updated_on", :type => :date_past
129 add_available_filter "updated_on", :type => :date_past
130 add_available_filter "closed_on", :type => :date_past
130 add_available_filter "closed_on", :type => :date_past
131 add_available_filter "start_date", :type => :date
131 add_available_filter "start_date", :type => :date
132 add_available_filter "due_date", :type => :date
132 add_available_filter "due_date", :type => :date
133 add_available_filter "estimated_hours", :type => :float
133 add_available_filter "estimated_hours", :type => :float
134 add_available_filter "done_ratio", :type => :integer
134 add_available_filter "done_ratio", :type => :integer
135
135
136 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
136 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
137 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
137 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
138 add_available_filter "is_private",
138 add_available_filter "is_private",
139 :type => :list,
139 :type => :list,
140 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
140 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
141 end
141 end
142
142
143 if User.current.logged?
143 if User.current.logged?
144 add_available_filter "watcher_id",
144 add_available_filter "watcher_id",
145 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
145 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
146 end
146 end
147
147
148 if project && !project.leaf?
148 if project && !project.leaf?
149 add_available_filter "subproject_id",
149 add_available_filter "subproject_id",
150 :type => :list_subprojects,
150 :type => :list_subprojects,
151 :values => lambda { subproject_values }
151 :values => lambda { subproject_values }
152 end
152 end
153
153
154
154
155 issue_custom_fields = project ? project.all_issue_custom_fields : IssueCustomField.where(:is_for_all => true)
155 issue_custom_fields = project ? project.all_issue_custom_fields : IssueCustomField.where(:is_for_all => true)
156 add_custom_fields_filters(issue_custom_fields)
156 add_custom_fields_filters(issue_custom_fields)
157
157
158 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
158 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
159
159
160 IssueRelation::TYPES.each do |relation_type, options|
160 IssueRelation::TYPES.each do |relation_type, options|
161 add_available_filter relation_type, :type => :relation, :label => options[:name], :values => lambda {all_projects_values}
161 add_available_filter relation_type, :type => :relation, :label => options[:name], :values => lambda {all_projects_values}
162 end
162 end
163 add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue
163 add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue
164 add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural
164 add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural
165
165
166 add_available_filter "issue_id", :type => :integer, :label => :label_issue
166 add_available_filter "issue_id", :type => :integer, :label => :label_issue
167
167
168 Tracker.disabled_core_fields(trackers).each {|field|
168 Tracker.disabled_core_fields(trackers).each {|field|
169 delete_available_filter field
169 delete_available_filter field
170 }
170 }
171 end
171 end
172
172
173 def available_columns
173 def available_columns
174 return @available_columns if @available_columns
174 return @available_columns if @available_columns
175 @available_columns = self.class.available_columns.dup
175 @available_columns = self.class.available_columns.dup
176 @available_columns += (project ?
176 @available_columns += (project ?
177 project.all_issue_custom_fields :
177 project.all_issue_custom_fields :
178 IssueCustomField
178 IssueCustomField
179 ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
179 ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
180
180
181 if User.current.allowed_to?(:view_time_entries, project, :global => true)
181 if User.current.allowed_to?(:view_time_entries, project, :global => true)
182 index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
182 index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
183 index = (index ? index + 1 : -1)
183 index = (index ? index + 1 : -1)
184 # insert the column after total_estimated_hours or at the end
184 # insert the column after total_estimated_hours or at the end
185 @available_columns.insert index, QueryColumn.new(:spent_hours,
185 @available_columns.insert index, QueryColumn.new(:spent_hours,
186 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
186 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
187 :default_order => 'desc',
187 :default_order => 'desc',
188 :caption => :label_spent_time,
188 :caption => :label_spent_time,
189 :totalable => true
189 :totalable => true
190 )
190 )
191 @available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
191 @available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
192 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" +
192 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" +
193 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
193 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
194 :default_order => 'desc',
194 :default_order => 'desc',
195 :caption => :label_total_spent_time
195 :caption => :label_total_spent_time
196 )
196 )
197 end
197 end
198
198
199 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
199 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
200 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
200 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
201 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
201 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
202 end
202 end
203
203
204 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
204 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
205 @available_columns.reject! {|column|
205 @available_columns.reject! {|column|
206 disabled_fields.include?(column.name.to_s)
206 disabled_fields.include?(column.name.to_s)
207 }
207 }
208
208
209 @available_columns
209 @available_columns
210 end
210 end
211
211
212 def default_columns_names
212 def default_columns_names
213 @default_columns_names ||= begin
213 @default_columns_names ||= begin
214 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
214 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
215
215
216 project.present? ? default_columns : [:project] | default_columns
216 project.present? ? default_columns : [:project] | default_columns
217 end
217 end
218 end
218 end
219
219
220 def default_totalable_names
220 def default_totalable_names
221 Setting.issue_list_default_totals.map(&:to_sym)
221 Setting.issue_list_default_totals.map(&:to_sym)
222 end
222 end
223
223
224 def base_scope
224 def base_scope
225 Issue.visible.joins(:status, :project).where(statement)
225 Issue.visible.joins(:status, :project).where(statement)
226 end
226 end
227
227
228 # Returns the issue count
228 # Returns the issue count
229 def issue_count
229 def issue_count
230 base_scope.count
230 base_scope.count
231 rescue ::ActiveRecord::StatementInvalid => e
231 rescue ::ActiveRecord::StatementInvalid => e
232 raise StatementInvalid.new(e.message)
232 raise StatementInvalid.new(e.message)
233 end
233 end
234
234
235 # Returns the issue count by group or nil if query is not grouped
235 # Returns the issue count by group or nil if query is not grouped
236 def issue_count_by_group
236 def issue_count_by_group
237 grouped_query do |scope|
237 grouped_query do |scope|
238 scope.count
238 scope.count
239 end
239 end
240 end
240 end
241
241
242 # Returns sum of all the issue's estimated_hours
242 # Returns sum of all the issue's estimated_hours
243 def total_for_estimated_hours(scope)
243 def total_for_estimated_hours(scope)
244 map_total(scope.sum(:estimated_hours)) {|t| t.to_f.round(2)}
244 map_total(scope.sum(:estimated_hours)) {|t| t.to_f.round(2)}
245 end
245 end
246
246
247 # Returns sum of all the issue's time entries hours
247 # Returns sum of all the issue's time entries hours
248 def total_for_spent_hours(scope)
248 def total_for_spent_hours(scope)
249 total = if group_by_column.try(:name) == :project
249 total = if group_by_column.try(:name) == :project
250 # TODO: remove this when https://github.com/rails/rails/issues/21922 is fixed
250 # TODO: remove this when https://github.com/rails/rails/issues/21922 is fixed
251 # We have to do a custom join without the time_entries.project_id column
251 # We have to do a custom join without the time_entries.project_id column
252 # that would trigger a ambiguous column name error
252 # that would trigger a ambiguous column name error
253 scope.joins("JOIN (SELECT issue_id, hours FROM #{TimeEntry.table_name}) AS joined_time_entries ON joined_time_entries.issue_id = #{Issue.table_name}.id").
253 scope.joins("JOIN (SELECT issue_id, hours FROM #{TimeEntry.table_name}) AS joined_time_entries ON joined_time_entries.issue_id = #{Issue.table_name}.id").
254 sum("joined_time_entries.hours")
254 sum("joined_time_entries.hours")
255 else
255 else
256 scope.joins(:time_entries).sum("#{TimeEntry.table_name}.hours")
256 scope.joins(:time_entries).sum("#{TimeEntry.table_name}.hours")
257 end
257 end
258 map_total(total) {|t| t.to_f.round(2)}
258 map_total(total) {|t| t.to_f.round(2)}
259 end
259 end
260
260
261 # Returns the issues
261 # Returns the issues
262 # Valid options are :order, :offset, :limit, :include, :conditions
262 # Valid options are :order, :offset, :limit, :include, :conditions
263 def issues(options={})
263 def issues(options={})
264 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
264 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
265
265
266 scope = Issue.visible.
266 scope = Issue.visible.
267 joins(:status, :project).
267 joins(:status, :project).
268 where(statement).
268 where(statement).
269 includes(([:status, :project] + (options[:include] || [])).uniq).
269 includes(([:status, :project] + (options[:include] || [])).uniq).
270 where(options[:conditions]).
270 where(options[:conditions]).
271 order(order_option).
271 order(order_option).
272 joins(joins_for_order_statement(order_option.join(','))).
272 joins(joins_for_order_statement(order_option.join(','))).
273 limit(options[:limit]).
273 limit(options[:limit]).
274 offset(options[:offset])
274 offset(options[:offset])
275
275
276 scope = scope.preload(:custom_values)
276 scope = scope.preload([:tracker, :priority, :author, :assigned_to, :fixed_version, :category] & columns.map(&:name))
277 if has_column?(:author)
277 if has_custom_field_column?
278 scope = scope.preload(:author)
278 scope = scope.preload(:custom_values)
279 end
279 end
280
280
281 issues = scope.to_a
281 issues = scope.to_a
282
282
283 if has_column?(:spent_hours)
283 if has_column?(:spent_hours)
284 Issue.load_visible_spent_hours(issues)
284 Issue.load_visible_spent_hours(issues)
285 end
285 end
286 if has_column?(:total_spent_hours)
286 if has_column?(:total_spent_hours)
287 Issue.load_visible_total_spent_hours(issues)
287 Issue.load_visible_total_spent_hours(issues)
288 end
288 end
289 if has_column?(:relations)
289 if has_column?(:relations)
290 Issue.load_visible_relations(issues)
290 Issue.load_visible_relations(issues)
291 end
291 end
292 issues
292 issues
293 rescue ::ActiveRecord::StatementInvalid => e
293 rescue ::ActiveRecord::StatementInvalid => e
294 raise StatementInvalid.new(e.message)
294 raise StatementInvalid.new(e.message)
295 end
295 end
296
296
297 # Returns the issues ids
297 # Returns the issues ids
298 def issue_ids(options={})
298 def issue_ids(options={})
299 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
299 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
300
300
301 Issue.visible.
301 Issue.visible.
302 joins(:status, :project).
302 joins(:status, :project).
303 where(statement).
303 where(statement).
304 includes(([:status, :project] + (options[:include] || [])).uniq).
304 includes(([:status, :project] + (options[:include] || [])).uniq).
305 references(([:status, :project] + (options[:include] || [])).uniq).
305 references(([:status, :project] + (options[:include] || [])).uniq).
306 where(options[:conditions]).
306 where(options[:conditions]).
307 order(order_option).
307 order(order_option).
308 joins(joins_for_order_statement(order_option.join(','))).
308 joins(joins_for_order_statement(order_option.join(','))).
309 limit(options[:limit]).
309 limit(options[:limit]).
310 offset(options[:offset]).
310 offset(options[:offset]).
311 pluck(:id)
311 pluck(:id)
312 rescue ::ActiveRecord::StatementInvalid => e
312 rescue ::ActiveRecord::StatementInvalid => e
313 raise StatementInvalid.new(e.message)
313 raise StatementInvalid.new(e.message)
314 end
314 end
315
315
316 # Returns the journals
316 # Returns the journals
317 # Valid options are :order, :offset, :limit
317 # Valid options are :order, :offset, :limit
318 def journals(options={})
318 def journals(options={})
319 Journal.visible.
319 Journal.visible.
320 joins(:issue => [:project, :status]).
320 joins(:issue => [:project, :status]).
321 where(statement).
321 where(statement).
322 order(options[:order]).
322 order(options[:order]).
323 limit(options[:limit]).
323 limit(options[:limit]).
324 offset(options[:offset]).
324 offset(options[:offset]).
325 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
325 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
326 to_a
326 to_a
327 rescue ::ActiveRecord::StatementInvalid => e
327 rescue ::ActiveRecord::StatementInvalid => e
328 raise StatementInvalid.new(e.message)
328 raise StatementInvalid.new(e.message)
329 end
329 end
330
330
331 # Returns the versions
331 # Returns the versions
332 # Valid options are :conditions
332 # Valid options are :conditions
333 def versions(options={})
333 def versions(options={})
334 Version.visible.
334 Version.visible.
335 where(project_statement).
335 where(project_statement).
336 where(options[:conditions]).
336 where(options[:conditions]).
337 includes(:project).
337 includes(:project).
338 references(:project).
338 references(:project).
339 to_a
339 to_a
340 rescue ::ActiveRecord::StatementInvalid => e
340 rescue ::ActiveRecord::StatementInvalid => e
341 raise StatementInvalid.new(e.message)
341 raise StatementInvalid.new(e.message)
342 end
342 end
343
343
344 def sql_for_watcher_id_field(field, operator, value)
344 def sql_for_watcher_id_field(field, operator, value)
345 db_table = Watcher.table_name
345 db_table = Watcher.table_name
346 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
346 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
347 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
347 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
348 end
348 end
349
349
350 def sql_for_member_of_group_field(field, operator, value)
350 def sql_for_member_of_group_field(field, operator, value)
351 if operator == '*' # Any group
351 if operator == '*' # Any group
352 groups = Group.givable
352 groups = Group.givable
353 operator = '=' # Override the operator since we want to find by assigned_to
353 operator = '=' # Override the operator since we want to find by assigned_to
354 elsif operator == "!*"
354 elsif operator == "!*"
355 groups = Group.givable
355 groups = Group.givable
356 operator = '!' # Override the operator since we want to find by assigned_to
356 operator = '!' # Override the operator since we want to find by assigned_to
357 else
357 else
358 groups = Group.where(:id => value).to_a
358 groups = Group.where(:id => value).to_a
359 end
359 end
360 groups ||= []
360 groups ||= []
361
361
362 members_of_groups = groups.inject([]) {|user_ids, group|
362 members_of_groups = groups.inject([]) {|user_ids, group|
363 user_ids + group.user_ids + [group.id]
363 user_ids + group.user_ids + [group.id]
364 }.uniq.compact.sort.collect(&:to_s)
364 }.uniq.compact.sort.collect(&:to_s)
365
365
366 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
366 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
367 end
367 end
368
368
369 def sql_for_assigned_to_role_field(field, operator, value)
369 def sql_for_assigned_to_role_field(field, operator, value)
370 case operator
370 case operator
371 when "*", "!*" # Member / Not member
371 when "*", "!*" # Member / Not member
372 sw = operator == "!*" ? 'NOT' : ''
372 sw = operator == "!*" ? 'NOT' : ''
373 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
373 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
374 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
374 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
375 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
375 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
376 when "=", "!"
376 when "=", "!"
377 role_cond = value.any? ?
377 role_cond = value.any? ?
378 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")" :
378 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")" :
379 "1=0"
379 "1=0"
380
380
381 sw = operator == "!" ? 'NOT' : ''
381 sw = operator == "!" ? 'NOT' : ''
382 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
382 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
383 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
383 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
384 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
384 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
385 end
385 end
386 end
386 end
387
387
388 def sql_for_fixed_version_status_field(field, operator, value)
388 def sql_for_fixed_version_status_field(field, operator, value)
389 where = sql_for_field(field, operator, value, Version.table_name, "status")
389 where = sql_for_field(field, operator, value, Version.table_name, "status")
390 version_ids = versions(:conditions => [where]).map(&:id)
390 version_ids = versions(:conditions => [where]).map(&:id)
391
391
392 nl = operator == "!" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
392 nl = operator == "!" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
393 "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
393 "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
394 end
394 end
395
395
396 def sql_for_fixed_version_due_date_field(field, operator, value)
396 def sql_for_fixed_version_due_date_field(field, operator, value)
397 where = sql_for_field(field, operator, value, Version.table_name, "effective_date")
397 where = sql_for_field(field, operator, value, Version.table_name, "effective_date")
398 version_ids = versions(:conditions => [where]).map(&:id)
398 version_ids = versions(:conditions => [where]).map(&:id)
399
399
400 nl = operator == "!*" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
400 nl = operator == "!*" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
401 "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
401 "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
402 end
402 end
403
403
404 def sql_for_is_private_field(field, operator, value)
404 def sql_for_is_private_field(field, operator, value)
405 op = (operator == "=" ? 'IN' : 'NOT IN')
405 op = (operator == "=" ? 'IN' : 'NOT IN')
406 va = value.map {|v| v == '0' ? self.class.connection.quoted_false : self.class.connection.quoted_true}.uniq.join(',')
406 va = value.map {|v| v == '0' ? self.class.connection.quoted_false : self.class.connection.quoted_true}.uniq.join(',')
407
407
408 "#{Issue.table_name}.is_private #{op} (#{va})"
408 "#{Issue.table_name}.is_private #{op} (#{va})"
409 end
409 end
410
410
411 def sql_for_parent_id_field(field, operator, value)
411 def sql_for_parent_id_field(field, operator, value)
412 case operator
412 case operator
413 when "="
413 when "="
414 "#{Issue.table_name}.parent_id = #{value.first.to_i}"
414 "#{Issue.table_name}.parent_id = #{value.first.to_i}"
415 when "~"
415 when "~"
416 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
416 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
417 if root_id && lft && rgt
417 if root_id && lft && rgt
418 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft > #{lft} AND #{Issue.table_name}.rgt < #{rgt}"
418 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft > #{lft} AND #{Issue.table_name}.rgt < #{rgt}"
419 else
419 else
420 "1=0"
420 "1=0"
421 end
421 end
422 when "!*"
422 when "!*"
423 "#{Issue.table_name}.parent_id IS NULL"
423 "#{Issue.table_name}.parent_id IS NULL"
424 when "*"
424 when "*"
425 "#{Issue.table_name}.parent_id IS NOT NULL"
425 "#{Issue.table_name}.parent_id IS NOT NULL"
426 end
426 end
427 end
427 end
428
428
429 def sql_for_child_id_field(field, operator, value)
429 def sql_for_child_id_field(field, operator, value)
430 case operator
430 case operator
431 when "="
431 when "="
432 parent_id = Issue.where(:id => value.first.to_i).pluck(:parent_id).first
432 parent_id = Issue.where(:id => value.first.to_i).pluck(:parent_id).first
433 if parent_id
433 if parent_id
434 "#{Issue.table_name}.id = #{parent_id}"
434 "#{Issue.table_name}.id = #{parent_id}"
435 else
435 else
436 "1=0"
436 "1=0"
437 end
437 end
438 when "~"
438 when "~"
439 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
439 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
440 if root_id && lft && rgt
440 if root_id && lft && rgt
441 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft < #{lft} AND #{Issue.table_name}.rgt > #{rgt}"
441 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft < #{lft} AND #{Issue.table_name}.rgt > #{rgt}"
442 else
442 else
443 "1=0"
443 "1=0"
444 end
444 end
445 when "!*"
445 when "!*"
446 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1"
446 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1"
447 when "*"
447 when "*"
448 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft > 1"
448 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft > 1"
449 end
449 end
450 end
450 end
451
451
452 def sql_for_issue_id_field(field, operator, value)
452 def sql_for_issue_id_field(field, operator, value)
453 if operator == "="
453 if operator == "="
454 # accepts a comma separated list of ids
454 # accepts a comma separated list of ids
455 ids = value.first.to_s.scan(/\d+/).map(&:to_i)
455 ids = value.first.to_s.scan(/\d+/).map(&:to_i)
456 if ids.present?
456 if ids.present?
457 "#{Issue.table_name}.id IN (#{ids.join(",")})"
457 "#{Issue.table_name}.id IN (#{ids.join(",")})"
458 else
458 else
459 "1=0"
459 "1=0"
460 end
460 end
461 else
461 else
462 sql_for_field("id", operator, value, Issue.table_name, "id")
462 sql_for_field("id", operator, value, Issue.table_name, "id")
463 end
463 end
464 end
464 end
465
465
466 def sql_for_relations(field, operator, value, options={})
466 def sql_for_relations(field, operator, value, options={})
467 relation_options = IssueRelation::TYPES[field]
467 relation_options = IssueRelation::TYPES[field]
468 return relation_options unless relation_options
468 return relation_options unless relation_options
469
469
470 relation_type = field
470 relation_type = field
471 join_column, target_join_column = "issue_from_id", "issue_to_id"
471 join_column, target_join_column = "issue_from_id", "issue_to_id"
472 if relation_options[:reverse] || options[:reverse]
472 if relation_options[:reverse] || options[:reverse]
473 relation_type = relation_options[:reverse] || relation_type
473 relation_type = relation_options[:reverse] || relation_type
474 join_column, target_join_column = target_join_column, join_column
474 join_column, target_join_column = target_join_column, join_column
475 end
475 end
476
476
477 sql = case operator
477 sql = case operator
478 when "*", "!*"
478 when "*", "!*"
479 op = (operator == "*" ? 'IN' : 'NOT IN')
479 op = (operator == "*" ? 'IN' : 'NOT IN')
480 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}')"
480 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}')"
481 when "=", "!"
481 when "=", "!"
482 op = (operator == "=" ? 'IN' : 'NOT IN')
482 op = (operator == "=" ? 'IN' : 'NOT IN')
483 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
483 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
484 when "=p", "=!p", "!p"
484 when "=p", "=!p", "!p"
485 op = (operator == "!p" ? 'NOT IN' : 'IN')
485 op = (operator == "!p" ? 'NOT IN' : 'IN')
486 comp = (operator == "=!p" ? '<>' : '=')
486 comp = (operator == "=!p" ? '<>' : '=')
487 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
487 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
488 when "*o", "!o"
488 when "*o", "!o"
489 op = (operator == "!o" ? 'NOT IN' : 'IN')
489 op = (operator == "!o" ? 'NOT IN' : 'IN')
490 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false}))"
490 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false}))"
491 end
491 end
492
492
493 if relation_options[:sym] == field && !options[:reverse]
493 if relation_options[:sym] == field && !options[:reverse]
494 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
494 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
495 sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
495 sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
496 end
496 end
497 "(#{sql})"
497 "(#{sql})"
498 end
498 end
499
499
500 def find_assigned_to_id_filter_values(values)
500 def find_assigned_to_id_filter_values(values)
501 Principal.visible.where(:id => values).map {|p| [p.name, p.id.to_s]}
501 Principal.visible.where(:id => values).map {|p| [p.name, p.id.to_s]}
502 end
502 end
503 alias :find_author_id_filter_values :find_assigned_to_id_filter_values
503 alias :find_author_id_filter_values :find_assigned_to_id_filter_values
504
504
505 IssueRelation::TYPES.keys.each do |relation_type|
505 IssueRelation::TYPES.keys.each do |relation_type|
506 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
506 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
507 end
507 end
508
508
509 def joins_for_order_statement(order_options)
509 def joins_for_order_statement(order_options)
510 joins = [super]
510 joins = [super]
511
511
512 if order_options
512 if order_options
513 if order_options.include?('authors')
513 if order_options.include?('authors')
514 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
514 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
515 end
515 end
516 if order_options.include?('users')
517 joins << "LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{queried_table_name}.assigned_to_id"
518 end
519 if order_options.include?('fixed_version')
520 joins << "LEFT OUTER JOIN #{Version.table_name} ON #{Version.table_name}.id = #{queried_table_name}.fixed_version_id"
521 end
522 if order_options.include?('category')
523 joins << "LEFT OUTER JOIN #{IssueCategory.table_name} ON #{IssueCategory.table_name}.id = #{queried_table_name}.category_id"
524 end
525 if order_options.include?('tracker')
526 joins << "LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{queried_table_name}.tracker_id"
527 end
528 if order_options.include?('enumeration')
529 joins << "LEFT OUTER JOIN #{IssuePriority.table_name} ON #{IssuePriority.table_name}.id = #{queried_table_name}.priority_id"
530 end
516 end
531 end
517
532
518 joins.any? ? joins.join(' ') : nil
533 joins.any? ? joins.join(' ') : nil
519 end
534 end
520 end
535 end
General Comments 0
You need to be logged in to leave comments. Login now