##// END OF EJS Templates
Refactor: Extract Query#sortable_columns from the controller....
Eric Davis -
r3490:6e6e260ceae6
parent child
Show More
@@ -1,588 +1,588
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => :new
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :update, :reply]
23 23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 24 before_filter :find_project, :only => [:new, :update_form, :preview, :auto_complete]
25 25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
26 26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
27 27 accept_key_auth :index, :show, :changes
28 28
29 29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30 30
31 31 helper :journals
32 32 helper :projects
33 33 include ProjectsHelper
34 34 helper :custom_fields
35 35 include CustomFieldsHelper
36 36 helper :issue_relations
37 37 include IssueRelationsHelper
38 38 helper :watchers
39 39 include WatchersHelper
40 40 helper :attachments
41 41 include AttachmentsHelper
42 42 helper :queries
43 43 include QueriesHelper
44 44 helper :sort
45 45 include SortHelper
46 46 include IssuesHelper
47 47 helper :timelog
48 48 include Redmine::Export::PDF
49 49
50 50 verify :method => [:post, :delete],
51 51 :only => :destroy,
52 52 :render => { :nothing => true, :status => :method_not_allowed }
53 53
54 54 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
55 55
56 56 def index
57 57 retrieve_query
58 58 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
59 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
59 sort_update(@query.sortable_columns)
60 60
61 61 if @query.valid?
62 62 limit = case params[:format]
63 63 when 'csv', 'pdf'
64 64 Setting.issues_export_limit.to_i
65 65 when 'atom'
66 66 Setting.feeds_limit.to_i
67 67 else
68 68 per_page_option
69 69 end
70 70
71 71 @issue_count = @query.issue_count
72 72 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
73 73 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
74 74 :order => sort_clause,
75 75 :offset => @issue_pages.current.offset,
76 76 :limit => limit)
77 77 @issue_count_by_group = @query.issue_count_by_group
78 78
79 79 respond_to do |format|
80 80 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
81 81 format.xml { render :layout => false }
82 82 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
83 83 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
84 84 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
85 85 end
86 86 else
87 87 # Send html if the query is not valid
88 88 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
89 89 end
90 90 rescue ActiveRecord::RecordNotFound
91 91 render_404
92 92 end
93 93
94 94 def changes
95 95 retrieve_query
96 96 sort_init 'id', 'desc'
97 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
97 sort_update(@query.sortable_columns)
98 98
99 99 if @query.valid?
100 100 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
101 101 :limit => 25)
102 102 end
103 103 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
104 104 render :layout => false, :content_type => 'application/atom+xml'
105 105 rescue ActiveRecord::RecordNotFound
106 106 render_404
107 107 end
108 108
109 109 def show
110 110 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
111 111 @journals.each_with_index {|j,i| j.indice = i+1}
112 112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
113 113 @changesets = @issue.changesets.visible.all
114 114 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
115 115 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
116 116 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
117 117 @priorities = IssuePriority.all
118 118 @time_entry = TimeEntry.new
119 119 respond_to do |format|
120 120 format.html { render :template => 'issues/show.rhtml' }
121 121 format.xml { render :layout => false }
122 122 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
123 123 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
124 124 end
125 125 end
126 126
127 127 # Add a new issue
128 128 # The new issue will be created from an existing one if copy_from parameter is given
129 129 def new
130 130 @issue = Issue.new
131 131 @issue.copy_from(params[:copy_from]) if params[:copy_from]
132 132 @issue.project = @project
133 133 # Tracker must be set before custom field values
134 134 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
135 135 if @issue.tracker.nil?
136 136 render_error l(:error_no_tracker_in_project)
137 137 return
138 138 end
139 139 if params[:issue].is_a?(Hash)
140 140 @issue.safe_attributes = params[:issue]
141 141 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
142 142 end
143 143 @issue.author = User.current
144 144
145 145 default_status = IssueStatus.default
146 146 unless default_status
147 147 render_error l(:error_no_default_issue_status)
148 148 return
149 149 end
150 150 @issue.status = default_status
151 151 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
152 152
153 153 if request.get? || request.xhr?
154 154 @issue.start_date ||= Date.today
155 155 else
156 156 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
157 157 # Check that the user is allowed to apply the requested status
158 158 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
159 159 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
160 160 if @issue.save
161 161 attachments = Attachment.attach_files(@issue, params[:attachments])
162 162 render_attachment_warning_if_needed(@issue)
163 163 flash[:notice] = l(:notice_successful_create)
164 164 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
165 165 respond_to do |format|
166 166 format.html {
167 167 redirect_to(params[:continue] ? { :action => 'new', :issue => {:tracker_id => @issue.tracker,
168 168 :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
169 169 { :action => 'show', :id => @issue })
170 170 }
171 171 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
172 172 end
173 173 return
174 174 else
175 175 respond_to do |format|
176 176 format.html { }
177 177 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
178 178 end
179 179 end
180 180 end
181 181 @priorities = IssuePriority.all
182 182 render :layout => !request.xhr?
183 183 end
184 184
185 185 # Attributes that can be updated on workflow transition (without :edit permission)
186 186 # TODO: make it configurable (at least per role)
187 187 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
188 188
189 189 def edit
190 190 update_issue_from_params
191 191
192 192 @journal = @issue.current_journal
193 193
194 194 respond_to do |format|
195 195 format.html { }
196 196 format.xml { }
197 197 end
198 198 end
199 199
200 200 def update
201 201 update_issue_from_params
202 202
203 203 if @issue.save_issue_with_child_records(params, @time_entry)
204 204 render_attachment_warning_if_needed(@issue)
205 205 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
206 206
207 207 respond_to do |format|
208 208 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
209 209 format.xml { head :ok }
210 210 end
211 211 else
212 212 render_attachment_warning_if_needed(@issue)
213 213 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
214 214 @journal = @issue.current_journal
215 215
216 216 respond_to do |format|
217 217 format.html { render :action => 'edit' }
218 218 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
219 219 end
220 220 end
221 221
222 222 rescue ActiveRecord::StaleObjectError
223 223 # Optimistic locking exception
224 224 flash.now[:error] = l(:notice_locking_conflict)
225 225 # Remove the previously added attachments if issue was not updated
226 226 attachments[:files].each(&:destroy) if attachments[:files]
227 227 end
228 228
229 229 def reply
230 230 journal = Journal.find(params[:journal_id]) if params[:journal_id]
231 231 if journal
232 232 user = journal.user
233 233 text = journal.notes
234 234 else
235 235 user = @issue.author
236 236 text = @issue.description
237 237 end
238 238 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
239 239 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
240 240 render(:update) { |page|
241 241 page.<< "$('notes').value = \"#{content}\";"
242 242 page.show 'update'
243 243 page << "Form.Element.focus('notes');"
244 244 page << "Element.scrollTo('update');"
245 245 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
246 246 }
247 247 end
248 248
249 249 # Bulk edit a set of issues
250 250 def bulk_edit
251 251 @issues.sort!
252 252 if request.post?
253 253 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
254 254 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
255 255 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
256 256
257 257 unsaved_issue_ids = []
258 258 @issues.each do |issue|
259 259 issue.reload
260 260 journal = issue.init_journal(User.current, params[:notes])
261 261 issue.safe_attributes = attributes
262 262 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
263 263 unless issue.save
264 264 # Keep unsaved issue ids to display them in flash error
265 265 unsaved_issue_ids << issue.id
266 266 end
267 267 end
268 268 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
269 269 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
270 270 return
271 271 end
272 272 @available_statuses = Workflow.available_statuses(@project)
273 273 @custom_fields = @project.all_issue_custom_fields
274 274 end
275 275
276 276 def move
277 277 @issues.sort!
278 278 @copy = params[:copy_options] && params[:copy_options][:copy]
279 279 @allowed_projects = []
280 280 # find projects to which the user is allowed to move the issue
281 281 if User.current.admin?
282 282 # admin is allowed to move issues to any active (visible) project
283 283 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
284 284 else
285 285 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
286 286 end
287 287 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
288 288 @target_project ||= @project
289 289 @trackers = @target_project.trackers
290 290 @available_statuses = Workflow.available_statuses(@project)
291 291 if request.post?
292 292 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
293 293 unsaved_issue_ids = []
294 294 moved_issues = []
295 295 @issues.each do |issue|
296 296 issue.reload
297 297 changed_attributes = {}
298 298 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
299 299 unless params[valid_attribute].blank?
300 300 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
301 301 end
302 302 end
303 303 issue.init_journal(User.current)
304 304 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
305 305 if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
306 306 moved_issues << r
307 307 else
308 308 unsaved_issue_ids << issue.id
309 309 end
310 310 end
311 311 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
312 312
313 313 if params[:follow]
314 314 if @issues.size == 1 && moved_issues.size == 1
315 315 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
316 316 else
317 317 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
318 318 end
319 319 else
320 320 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
321 321 end
322 322 return
323 323 end
324 324 render :layout => false if request.xhr?
325 325 end
326 326
327 327 def destroy
328 328 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
329 329 if @hours > 0
330 330 case params[:todo]
331 331 when 'destroy'
332 332 # nothing to do
333 333 when 'nullify'
334 334 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
335 335 when 'reassign'
336 336 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
337 337 if reassign_to.nil?
338 338 flash.now[:error] = l(:error_issue_not_found_in_project)
339 339 return
340 340 else
341 341 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
342 342 end
343 343 else
344 344 unless params[:format] == 'xml'
345 345 # display the destroy form if it's a user request
346 346 return
347 347 end
348 348 end
349 349 end
350 350 @issues.each(&:destroy)
351 351 respond_to do |format|
352 352 format.html { redirect_to :action => 'index', :project_id => @project }
353 353 format.xml { head :ok }
354 354 end
355 355 end
356 356
357 357 def gantt
358 358 @gantt = Redmine::Helpers::Gantt.new(params)
359 359 retrieve_query
360 360 @query.group_by = nil
361 361 if @query.valid?
362 362 events = []
363 363 # Issues that have start and due dates
364 364 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
365 365 :order => "start_date, due_date",
366 366 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
367 367 )
368 368 # Issues that don't have a due date but that are assigned to a version with a date
369 369 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
370 370 :order => "start_date, effective_date",
371 371 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
372 372 )
373 373 # Versions
374 374 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
375 375
376 376 @gantt.events = events
377 377 end
378 378
379 379 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
380 380
381 381 respond_to do |format|
382 382 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
383 383 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
384 384 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
385 385 end
386 386 end
387 387
388 388 def calendar
389 389 if params[:year] and params[:year].to_i > 1900
390 390 @year = params[:year].to_i
391 391 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
392 392 @month = params[:month].to_i
393 393 end
394 394 end
395 395 @year ||= Date.today.year
396 396 @month ||= Date.today.month
397 397
398 398 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
399 399 retrieve_query
400 400 @query.group_by = nil
401 401 if @query.valid?
402 402 events = []
403 403 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
404 404 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
405 405 )
406 406 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
407 407
408 408 @calendar.events = events
409 409 end
410 410
411 411 render :layout => false if request.xhr?
412 412 end
413 413
414 414 def context_menu
415 415 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
416 416 if (@issues.size == 1)
417 417 @issue = @issues.first
418 418 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
419 419 end
420 420 projects = @issues.collect(&:project).compact.uniq
421 421 @project = projects.first if projects.size == 1
422 422
423 423 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
424 424 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
425 425 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
426 426 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
427 427 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
428 428 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
429 429 }
430 430 if @project
431 431 @assignables = @project.assignable_users
432 432 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
433 433 @trackers = @project.trackers
434 434 end
435 435
436 436 @priorities = IssuePriority.all.reverse
437 437 @statuses = IssueStatus.find(:all, :order => 'position')
438 438 @back = params[:back_url] || request.env['HTTP_REFERER']
439 439
440 440 render :layout => false
441 441 end
442 442
443 443 def update_form
444 444 if params[:id].blank?
445 445 @issue = Issue.new
446 446 @issue.project = @project
447 447 else
448 448 @issue = @project.issues.visible.find(params[:id])
449 449 end
450 450 @issue.attributes = params[:issue]
451 451 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
452 452 @priorities = IssuePriority.all
453 453
454 454 render :partial => 'attributes'
455 455 end
456 456
457 457 def preview
458 458 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
459 459 @attachements = @issue.attachments if @issue
460 460 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
461 461 render :partial => 'common/preview'
462 462 end
463 463
464 464 def auto_complete
465 465 @issues = []
466 466 q = params[:q].to_s
467 467 if q.match(/^\d+$/)
468 468 @issues << @project.issues.visible.find_by_id(q.to_i)
469 469 end
470 470 unless q.blank?
471 471 @issues += @project.issues.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
472 472 end
473 473 render :layout => false
474 474 end
475 475
476 476 private
477 477 def find_issue
478 478 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
479 479 @project = @issue.project
480 480 rescue ActiveRecord::RecordNotFound
481 481 render_404
482 482 end
483 483
484 484 # Filter for bulk operations
485 485 def find_issues
486 486 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
487 487 raise ActiveRecord::RecordNotFound if @issues.empty?
488 488 projects = @issues.collect(&:project).compact.uniq
489 489 if projects.size == 1
490 490 @project = projects.first
491 491 else
492 492 # TODO: let users bulk edit/move/destroy issues from different projects
493 493 render_error 'Can not bulk edit/move/destroy issues from different projects'
494 494 return false
495 495 end
496 496 rescue ActiveRecord::RecordNotFound
497 497 render_404
498 498 end
499 499
500 500 def find_project
501 501 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
502 502 @project = Project.find(project_id)
503 503 rescue ActiveRecord::RecordNotFound
504 504 render_404
505 505 end
506 506
507 507 def find_optional_project
508 508 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
509 509 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
510 510 allowed ? true : deny_access
511 511 rescue ActiveRecord::RecordNotFound
512 512 render_404
513 513 end
514 514
515 515 # Retrieve query from session or build a new query
516 516 def retrieve_query
517 517 if !params[:query_id].blank?
518 518 cond = "project_id IS NULL"
519 519 cond << " OR project_id = #{@project.id}" if @project
520 520 @query = Query.find(params[:query_id], :conditions => cond)
521 521 @query.project = @project
522 522 session[:query] = {:id => @query.id, :project_id => @query.project_id}
523 523 sort_clear
524 524 else
525 525 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
526 526 # Give it a name, required to be valid
527 527 @query = Query.new(:name => "_")
528 528 @query.project = @project
529 529 if params[:fields] and params[:fields].is_a? Array
530 530 params[:fields].each do |field|
531 531 @query.add_filter(field, params[:operators][field], params[:values][field])
532 532 end
533 533 else
534 534 @query.available_filters.keys.each do |field|
535 535 @query.add_short_filter(field, params[field]) if params[field]
536 536 end
537 537 end
538 538 @query.group_by = params[:group_by]
539 539 @query.column_names = params[:query] && params[:query][:column_names]
540 540 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
541 541 else
542 542 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
543 543 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
544 544 @query.project = @project
545 545 end
546 546 end
547 547 end
548 548
549 549 # Rescues an invalid query statement. Just in case...
550 550 def query_statement_invalid(exception)
551 551 logger.error "Query::StatementInvalid: #{exception.message}" if logger
552 552 session.delete(:query)
553 553 sort_clear
554 554 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
555 555 end
556 556
557 557 # Used by #edit and #update to set some common instance variables
558 558 # from the params
559 559 # TODO: Refactor, not everything in here is needed by #edit
560 560 def update_issue_from_params
561 561 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
562 562 @priorities = IssuePriority.all
563 563 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
564 564 @time_entry = TimeEntry.new
565 565
566 566 @notes = params[:notes]
567 567 @issue.init_journal(User.current, @notes)
568 568 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
569 569 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
570 570 attrs = params[:issue].dup
571 571 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
572 572 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
573 573 @issue.safe_attributes = attrs
574 574 end
575 575
576 576 end
577 577
578 578 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
579 579 if unsaved_issue_ids.empty?
580 580 flash[:notice] = l(:notice_successful_update) unless issues.empty?
581 581 else
582 582 flash[:error] = l(:notice_failed_to_save_issues,
583 583 :count => unsaved_issue_ids.size,
584 584 :total => issues.size,
585 585 :ids => '#' + unsaved_issue_ids.join(', #'))
586 586 end
587 587 end
588 588 end
@@ -1,563 +1,571
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.default_order = options[:default_order]
30 30 end
31 31
32 32 def caption
33 33 l("field_#{name}")
34 34 end
35 35
36 36 # Returns true if the column is sortable, otherwise false
37 37 def sortable?
38 38 !sortable.nil?
39 39 end
40 40
41 41 def value(issue)
42 42 issue.send name
43 43 end
44 44 end
45 45
46 46 class QueryCustomFieldColumn < QueryColumn
47 47
48 48 def initialize(custom_field)
49 49 self.name = "cf_#{custom_field.id}".to_sym
50 50 self.sortable = custom_field.order_statement || false
51 51 if %w(list date bool int).include?(custom_field.field_format)
52 52 self.groupable = custom_field.order_statement
53 53 end
54 54 self.groupable ||= false
55 55 @cf = custom_field
56 56 end
57 57
58 58 def caption
59 59 @cf.name
60 60 end
61 61
62 62 def custom_field
63 63 @cf
64 64 end
65 65
66 66 def value(issue)
67 67 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
68 68 cv && @cf.cast_value(cv.value)
69 69 end
70 70 end
71 71
72 72 class Query < ActiveRecord::Base
73 73 class StatementInvalid < ::ActiveRecord::StatementInvalid
74 74 end
75 75
76 76 belongs_to :project
77 77 belongs_to :user
78 78 serialize :filters
79 79 serialize :column_names
80 80 serialize :sort_criteria, Array
81 81
82 82 attr_protected :project_id, :user_id
83 83
84 84 validates_presence_of :name, :on => :save
85 85 validates_length_of :name, :maximum => 255
86 86
87 87 @@operators = { "=" => :label_equals,
88 88 "!" => :label_not_equals,
89 89 "o" => :label_open_issues,
90 90 "c" => :label_closed_issues,
91 91 "!*" => :label_none,
92 92 "*" => :label_all,
93 93 ">=" => :label_greater_or_equal,
94 94 "<=" => :label_less_or_equal,
95 95 "<t+" => :label_in_less_than,
96 96 ">t+" => :label_in_more_than,
97 97 "t+" => :label_in,
98 98 "t" => :label_today,
99 99 "w" => :label_this_week,
100 100 ">t-" => :label_less_than_ago,
101 101 "<t-" => :label_more_than_ago,
102 102 "t-" => :label_ago,
103 103 "~" => :label_contains,
104 104 "!~" => :label_not_contains }
105 105
106 106 cattr_reader :operators
107 107
108 108 @@operators_by_filter_type = { :list => [ "=", "!" ],
109 109 :list_status => [ "o", "=", "!", "c", "*" ],
110 110 :list_optional => [ "=", "!", "!*", "*" ],
111 111 :list_subprojects => [ "*", "!*", "=" ],
112 112 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
113 113 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
114 114 :string => [ "=", "~", "!", "!~" ],
115 115 :text => [ "~", "!~" ],
116 116 :integer => [ "=", ">=", "<=", "!*", "*" ] }
117 117
118 118 cattr_reader :operators_by_filter_type
119 119
120 120 @@available_columns = [
121 121 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
122 122 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
123 123 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
124 124 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
125 125 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
126 126 QueryColumn.new(:author),
127 127 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
128 128 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
129 129 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
130 130 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
131 131 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
132 132 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
133 133 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
134 134 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
135 135 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
136 136 ]
137 137 cattr_reader :available_columns
138 138
139 139 def initialize(attributes = nil)
140 140 super attributes
141 141 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
142 142 end
143 143
144 144 def after_initialize
145 145 # Store the fact that project is nil (used in #editable_by?)
146 146 @is_for_all = project.nil?
147 147 end
148 148
149 149 def validate
150 150 filters.each_key do |field|
151 151 errors.add label_for(field), :blank unless
152 152 # filter requires one or more values
153 153 (values_for(field) and !values_for(field).first.blank?) or
154 154 # filter doesn't require any value
155 155 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
156 156 end if filters
157 157 end
158 158
159 159 def editable_by?(user)
160 160 return false unless user
161 161 # Admin can edit them all and regular users can edit their private queries
162 162 return true if user.admin? || (!is_public && self.user_id == user.id)
163 163 # Members can not edit public queries that are for all project (only admin is allowed to)
164 164 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
165 165 end
166 166
167 167 def available_filters
168 168 return @available_filters if @available_filters
169 169
170 170 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
171 171
172 172 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
173 173 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
174 174 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
175 175 "subject" => { :type => :text, :order => 8 },
176 176 "created_on" => { :type => :date_past, :order => 9 },
177 177 "updated_on" => { :type => :date_past, :order => 10 },
178 178 "start_date" => { :type => :date, :order => 11 },
179 179 "due_date" => { :type => :date, :order => 12 },
180 180 "estimated_hours" => { :type => :integer, :order => 13 },
181 181 "done_ratio" => { :type => :integer, :order => 14 }}
182 182
183 183 user_values = []
184 184 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
185 185 if project
186 186 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
187 187 else
188 188 # members of the user's projects
189 189 # OPTIMIZE: Is selecting from users per project (N+1)
190 190 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
191 191 end
192 192 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
193 193 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
194 194
195 195 if User.current.logged?
196 196 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
197 197 end
198 198
199 199 if project
200 200 # project specific filters
201 201 unless @project.issue_categories.empty?
202 202 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
203 203 end
204 204 unless @project.shared_versions.empty?
205 205 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
206 206 end
207 207 unless @project.descendants.active.empty?
208 208 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
209 209 end
210 210 add_custom_fields_filters(@project.all_issue_custom_fields)
211 211 else
212 212 # global filters for cross project issue list
213 213 system_shared_versions = Version.visible.find_all_by_sharing('system')
214 214 unless system_shared_versions.empty?
215 215 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
216 216 end
217 217 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
218 218 end
219 219 @available_filters
220 220 end
221 221
222 222 def add_filter(field, operator, values)
223 223 # values must be an array
224 224 return unless values and values.is_a? Array # and !values.first.empty?
225 225 # check if field is defined as an available filter
226 226 if available_filters.has_key? field
227 227 filter_options = available_filters[field]
228 228 # check if operator is allowed for that filter
229 229 #if @@operators_by_filter_type[filter_options[:type]].include? operator
230 230 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
231 231 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
232 232 #end
233 233 filters[field] = {:operator => operator, :values => values }
234 234 end
235 235 end
236 236
237 237 def add_short_filter(field, expression)
238 238 return unless expression
239 239 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
240 240 add_filter field, (parms[0] || "="), [parms[1] || ""]
241 241 end
242 242
243 243 def has_filter?(field)
244 244 filters and filters[field]
245 245 end
246 246
247 247 def operator_for(field)
248 248 has_filter?(field) ? filters[field][:operator] : nil
249 249 end
250 250
251 251 def values_for(field)
252 252 has_filter?(field) ? filters[field][:values] : nil
253 253 end
254 254
255 255 def label_for(field)
256 256 label = available_filters[field][:name] if available_filters.has_key?(field)
257 257 label ||= field.gsub(/\_id$/, "")
258 258 end
259 259
260 260 def available_columns
261 261 return @available_columns if @available_columns
262 262 @available_columns = Query.available_columns
263 263 @available_columns += (project ?
264 264 project.all_issue_custom_fields :
265 265 IssueCustomField.find(:all)
266 266 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
267 267 end
268 268
269 269 # Returns an array of columns that can be used to group the results
270 270 def groupable_columns
271 271 available_columns.select {|c| c.groupable}
272 272 end
273 273
274 # Returns a Hash of columns and the key for sorting
275 def sortable_columns
276 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
277 h[column.name.to_s] = column.sortable
278 h
279 })
280 end
281
274 282 def columns
275 283 if has_default_columns?
276 284 available_columns.select do |c|
277 285 # Adds the project column by default for cross-project lists
278 286 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
279 287 end
280 288 else
281 289 # preserve the column_names order
282 290 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
283 291 end
284 292 end
285 293
286 294 def column_names=(names)
287 295 if names
288 296 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
289 297 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
290 298 # Set column_names to nil if default columns
291 299 if names.map(&:to_s) == Setting.issue_list_default_columns
292 300 names = nil
293 301 end
294 302 end
295 303 write_attribute(:column_names, names)
296 304 end
297 305
298 306 def has_column?(column)
299 307 column_names && column_names.include?(column.name)
300 308 end
301 309
302 310 def has_default_columns?
303 311 column_names.nil? || column_names.empty?
304 312 end
305 313
306 314 def sort_criteria=(arg)
307 315 c = []
308 316 if arg.is_a?(Hash)
309 317 arg = arg.keys.sort.collect {|k| arg[k]}
310 318 end
311 319 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
312 320 write_attribute(:sort_criteria, c)
313 321 end
314 322
315 323 def sort_criteria
316 324 read_attribute(:sort_criteria) || []
317 325 end
318 326
319 327 def sort_criteria_key(arg)
320 328 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
321 329 end
322 330
323 331 def sort_criteria_order(arg)
324 332 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
325 333 end
326 334
327 335 # Returns the SQL sort order that should be prepended for grouping
328 336 def group_by_sort_order
329 337 if grouped? && (column = group_by_column)
330 338 column.sortable.is_a?(Array) ?
331 339 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
332 340 "#{column.sortable} #{column.default_order}"
333 341 end
334 342 end
335 343
336 344 # Returns true if the query is a grouped query
337 345 def grouped?
338 346 !group_by.blank?
339 347 end
340 348
341 349 def group_by_column
342 350 groupable_columns.detect {|c| c.name.to_s == group_by}
343 351 end
344 352
345 353 def group_by_statement
346 354 group_by_column.groupable
347 355 end
348 356
349 357 def project_statement
350 358 project_clauses = []
351 359 if project && !@project.descendants.active.empty?
352 360 ids = [project.id]
353 361 if has_filter?("subproject_id")
354 362 case operator_for("subproject_id")
355 363 when '='
356 364 # include the selected subprojects
357 365 ids += values_for("subproject_id").each(&:to_i)
358 366 when '!*'
359 367 # main project only
360 368 else
361 369 # all subprojects
362 370 ids += project.descendants.collect(&:id)
363 371 end
364 372 elsif Setting.display_subprojects_issues?
365 373 ids += project.descendants.collect(&:id)
366 374 end
367 375 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
368 376 elsif project
369 377 project_clauses << "#{Project.table_name}.id = %d" % project.id
370 378 end
371 379 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
372 380 project_clauses.join(' AND ')
373 381 end
374 382
375 383 def statement
376 384 # filters clauses
377 385 filters_clauses = []
378 386 filters.each_key do |field|
379 387 next if field == "subproject_id"
380 388 v = values_for(field).clone
381 389 next unless v and !v.empty?
382 390 operator = operator_for(field)
383 391
384 392 # "me" value subsitution
385 393 if %w(assigned_to_id author_id watcher_id).include?(field)
386 394 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
387 395 end
388 396
389 397 sql = ''
390 398 if field =~ /^cf_(\d+)$/
391 399 # custom field
392 400 db_table = CustomValue.table_name
393 401 db_field = 'value'
394 402 is_custom_filter = true
395 403 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
396 404 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
397 405 elsif field == 'watcher_id'
398 406 db_table = Watcher.table_name
399 407 db_field = 'user_id'
400 408 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
401 409 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
402 410 else
403 411 # regular field
404 412 db_table = Issue.table_name
405 413 db_field = field
406 414 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
407 415 end
408 416 filters_clauses << sql
409 417
410 418 end if filters and valid?
411 419
412 420 (filters_clauses << project_statement).join(' AND ')
413 421 end
414 422
415 423 # Returns the issue count
416 424 def issue_count
417 425 Issue.count(:include => [:status, :project], :conditions => statement)
418 426 rescue ::ActiveRecord::StatementInvalid => e
419 427 raise StatementInvalid.new(e.message)
420 428 end
421 429
422 430 # Returns the issue count by group or nil if query is not grouped
423 431 def issue_count_by_group
424 432 r = nil
425 433 if grouped?
426 434 begin
427 435 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
428 436 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
429 437 rescue ActiveRecord::RecordNotFound
430 438 r = {nil => issue_count}
431 439 end
432 440 c = group_by_column
433 441 if c.is_a?(QueryCustomFieldColumn)
434 442 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
435 443 end
436 444 end
437 445 r
438 446 rescue ::ActiveRecord::StatementInvalid => e
439 447 raise StatementInvalid.new(e.message)
440 448 end
441 449
442 450 # Returns the issues
443 451 # Valid options are :order, :offset, :limit, :include, :conditions
444 452 def issues(options={})
445 453 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
446 454 order_option = nil if order_option.blank?
447 455
448 456 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
449 457 :conditions => Query.merge_conditions(statement, options[:conditions]),
450 458 :order => order_option,
451 459 :limit => options[:limit],
452 460 :offset => options[:offset]
453 461 rescue ::ActiveRecord::StatementInvalid => e
454 462 raise StatementInvalid.new(e.message)
455 463 end
456 464
457 465 # Returns the journals
458 466 # Valid options are :order, :offset, :limit
459 467 def journals(options={})
460 468 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
461 469 :conditions => statement,
462 470 :order => options[:order],
463 471 :limit => options[:limit],
464 472 :offset => options[:offset]
465 473 rescue ::ActiveRecord::StatementInvalid => e
466 474 raise StatementInvalid.new(e.message)
467 475 end
468 476
469 477 # Returns the versions
470 478 # Valid options are :conditions
471 479 def versions(options={})
472 480 Version.find :all, :include => :project,
473 481 :conditions => Query.merge_conditions(project_statement, options[:conditions])
474 482 rescue ::ActiveRecord::StatementInvalid => e
475 483 raise StatementInvalid.new(e.message)
476 484 end
477 485
478 486 private
479 487
480 488 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
481 489 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
482 490 sql = ''
483 491 case operator
484 492 when "="
485 493 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
486 494 when "!"
487 495 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
488 496 when "!*"
489 497 sql = "#{db_table}.#{db_field} IS NULL"
490 498 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
491 499 when "*"
492 500 sql = "#{db_table}.#{db_field} IS NOT NULL"
493 501 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
494 502 when ">="
495 503 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
496 504 when "<="
497 505 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
498 506 when "o"
499 507 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
500 508 when "c"
501 509 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
502 510 when ">t-"
503 511 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
504 512 when "<t-"
505 513 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
506 514 when "t-"
507 515 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
508 516 when ">t+"
509 517 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
510 518 when "<t+"
511 519 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
512 520 when "t+"
513 521 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
514 522 when "t"
515 523 sql = date_range_clause(db_table, db_field, 0, 0)
516 524 when "w"
517 525 from = l(:general_first_day_of_week) == '7' ?
518 526 # week starts on sunday
519 527 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
520 528 # week starts on monday (Rails default)
521 529 Time.now.at_beginning_of_week
522 530 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
523 531 when "~"
524 532 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
525 533 when "!~"
526 534 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
527 535 end
528 536
529 537 return sql
530 538 end
531 539
532 540 def add_custom_fields_filters(custom_fields)
533 541 @available_filters ||= {}
534 542
535 543 custom_fields.select(&:is_filter?).each do |field|
536 544 case field.field_format
537 545 when "text"
538 546 options = { :type => :text, :order => 20 }
539 547 when "list"
540 548 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
541 549 when "date"
542 550 options = { :type => :date, :order => 20 }
543 551 when "bool"
544 552 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
545 553 else
546 554 options = { :type => :string, :order => 20 }
547 555 end
548 556 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
549 557 end
550 558 end
551 559
552 560 # Returns a SQL clause for a date or datetime field.
553 561 def date_range_clause(table, field, from, to)
554 562 s = []
555 563 if from
556 564 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
557 565 end
558 566 if to
559 567 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
560 568 end
561 569 s.join(' AND ')
562 570 end
563 571 end
General Comments 0
You need to be logged in to leave comments. Login now