comparison app/models/query.rb @ 514:7eba09d624db live

Merge
author Chris Cannam
date Thu, 14 Jul 2011 10:50:53 +0100
parents 0c939c159af4
children cbb26bc654de
comparison
equal deleted inserted replaced
512:b9aebdd7dd40 514:7eba09d624db
1 # Redmine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang 2 # Copyright (C) 2006-2011 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 QueryColumn 18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order 19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n 20 include Redmine::I18n
21 21
22 def initialize(name, options={}) 22 def initialize(name, options={})
23 self.name = name 23 self.name = name
24 self.sortable = options[:sortable] 24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false 25 self.groupable = options[:groupable] || false
26 if groupable == true 26 if groupable == true
27 self.groupable = name.to_s 27 self.groupable = name.to_s
28 end 28 end
29 self.default_order = options[:default_order] 29 self.default_order = options[:default_order]
30 @caption_key = options[:caption] || "field_#{name}" 30 @caption_key = options[:caption] || "field_#{name}"
31 end 31 end
32 32
33 def caption 33 def caption
34 l(@caption_key) 34 l(@caption_key)
35 end 35 end
36 36
37 # Returns true if the column is sortable, otherwise false 37 # Returns true if the column is sortable, otherwise false
38 def sortable? 38 def sortable?
39 !sortable.nil? 39 !sortable.nil?
40 end 40 end
41 41
42 def value(issue) 42 def value(issue)
43 issue.send name 43 issue.send name
44 end
45
46 def css_classes
47 name
44 end 48 end
45 end 49 end
46 50
47 class QueryCustomFieldColumn < QueryColumn 51 class QueryCustomFieldColumn < QueryColumn
48 52
53 self.groupable = custom_field.order_statement 57 self.groupable = custom_field.order_statement
54 end 58 end
55 self.groupable ||= false 59 self.groupable ||= false
56 @cf = custom_field 60 @cf = custom_field
57 end 61 end
58 62
59 def caption 63 def caption
60 @cf.name 64 @cf.name
61 end 65 end
62 66
63 def custom_field 67 def custom_field
64 @cf 68 @cf
65 end 69 end
66 70
67 def value(issue) 71 def value(issue)
68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id} 72 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
69 cv && @cf.cast_value(cv.value) 73 cv && @cf.cast_value(cv.value)
70 end 74 end
75
76 def css_classes
77 @css_classes ||= "#{name} #{@cf.field_format}"
78 end
71 end 79 end
72 80
73 class Query < ActiveRecord::Base 81 class Query < ActiveRecord::Base
74 class StatementInvalid < ::ActiveRecord::StatementInvalid 82 class StatementInvalid < ::ActiveRecord::StatementInvalid
75 end 83 end
76 84
77 belongs_to :project 85 belongs_to :project
78 belongs_to :user 86 belongs_to :user
79 serialize :filters 87 serialize :filters
80 serialize :column_names 88 serialize :column_names
81 serialize :sort_criteria, Array 89 serialize :sort_criteria, Array
82 90
83 attr_protected :project_id, :user_id 91 attr_protected :project_id, :user_id
84 92
85 validates_presence_of :name, :on => :save 93 validates_presence_of :name, :on => :save
86 validates_length_of :name, :maximum => 255 94 validates_length_of :name, :maximum => 255
87 95
88 @@operators = { "=" => :label_equals, 96 @@operators = { "=" => :label_equals,
89 "!" => :label_not_equals, 97 "!" => :label_not_equals,
90 "o" => :label_open_issues, 98 "o" => :label_open_issues,
91 "c" => :label_closed_issues, 99 "c" => :label_closed_issues,
92 "!*" => :label_none, 100 "!*" => :label_none,
93 "*" => :label_all, 101 "*" => :label_all,
103 "t-" => :label_ago, 111 "t-" => :label_ago,
104 "~" => :label_contains, 112 "~" => :label_contains,
105 "!~" => :label_not_contains } 113 "!~" => :label_not_contains }
106 114
107 cattr_reader :operators 115 cattr_reader :operators
108 116
109 @@operators_by_filter_type = { :list => [ "=", "!" ], 117 @@operators_by_filter_type = { :list => [ "=", "!" ],
110 :list_status => [ "o", "=", "!", "c", "*" ], 118 :list_status => [ "o", "=", "!", "c", "*" ],
111 :list_optional => [ "=", "!", "!*", "*" ], 119 :list_optional => [ "=", "!", "!*", "*" ],
112 :list_subprojects => [ "*", "!*", "=" ], 120 :list_subprojects => [ "*", "!*", "=" ],
113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ], 121 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), 143 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), 144 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), 145 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
138 ] 146 ]
139 cattr_reader :available_columns 147 cattr_reader :available_columns
140 148
141 def initialize(attributes = nil) 149 def initialize(attributes = nil)
142 super attributes 150 super attributes
143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } 151 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
144 end 152 end
145 153
146 def after_initialize 154 def after_initialize
147 # Store the fact that project is nil (used in #editable_by?) 155 # Store the fact that project is nil (used in #editable_by?)
148 @is_for_all = project.nil? 156 @is_for_all = project.nil?
149 end 157 end
150 158
151 def validate 159 def validate
152 filters.each_key do |field| 160 filters.each_key do |field|
153 errors.add label_for(field), :blank unless 161 errors.add label_for(field), :blank unless
154 # filter requires one or more values 162 # filter requires one or more values
155 (values_for(field) and !values_for(field).first.blank?) or 163 (values_for(field) and !values_for(field).first.blank?) or
156 # filter doesn't require any value 164 # filter doesn't require any value
157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field) 165 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
158 end if filters 166 end if filters
159 end 167 end
160 168
169 # Returns true if the query is visible to +user+ or the current user.
170 def visible?(user=User.current)
171 self.is_public? || self.user_id == user.id
172 end
173
161 def editable_by?(user) 174 def editable_by?(user)
162 return false unless user 175 return false unless user
163 # Admin can edit them all and regular users can edit their private queries 176 # Admin can edit them all and regular users can edit their private queries
164 return true if user.admin? || (!is_public && self.user_id == user.id) 177 return true if user.admin? || (!is_public && self.user_id == user.id)
165 # Members can not edit public queries that are for all project (only admin is allowed to) 178 # Members can not edit public queries that are for all project (only admin is allowed to)
166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project) 179 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
167 end 180 end
168 181
169 def available_filters 182 def available_filters
170 return @available_filters if @available_filters 183 return @available_filters if @available_filters
171 184
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers 185 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
173 186
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, 187 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } }, 188 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } }, 189 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
177 "subject" => { :type => :text, :order => 8 }, 190 "subject" => { :type => :text, :order => 8 },
178 "created_on" => { :type => :date_past, :order => 9 }, 191 "created_on" => { :type => :date_past, :order => 9 },
179 "updated_on" => { :type => :date_past, :order => 10 }, 192 "updated_on" => { :type => :date_past, :order => 10 },
180 "start_date" => { :type => :date, :order => 11 }, 193 "start_date" => { :type => :date, :order => 11 },
181 "due_date" => { :type => :date, :order => 12 }, 194 "due_date" => { :type => :date, :order => 12 },
182 "estimated_hours" => { :type => :integer, :order => 13 }, 195 "estimated_hours" => { :type => :integer, :order => 13 },
183 "done_ratio" => { :type => :integer, :order => 14 }} 196 "done_ratio" => { :type => :integer, :order => 14 }}
184 197
185 user_values = [] 198 user_values = []
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? 199 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
187 if project 200 if project
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } 201 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
189 else 202 else
190 project_ids = Project.all(:conditions => Project.visible_by(User.current)).collect(&:id) 203 all_projects = Project.visible.all
191 if project_ids.any? 204 if all_projects.any?
192 # members of the user's projects 205 # members of visible projects
193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] } 206 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
207
208 # project filter
209 project_values = []
210 Project.project_tree(all_projects) do |p, level|
211 prefix = (level > 0 ? ('--' * level + ' ') : '')
212 project_values << ["#{prefix}#{p.name}", p.id.to_s]
213 end
214 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
194 end 215 end
195 end 216 end
196 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty? 217 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
197 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty? 218 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
198 219
199 group_values = Group.all.collect {|g| [g.name, g.id.to_s] } 220 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
200 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty? 221 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
201 222
202 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } 223 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
203 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty? 224 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
204 225
205 if User.current.logged? 226 if User.current.logged?
206 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] } 227 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
207 end 228 end
208 229
209 if project 230 if project
210 # project specific filters 231 # project specific filters
211 unless @project.issue_categories.empty? 232 categories = @project.issue_categories.all
212 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } 233 unless categories.empty?
213 end 234 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
214 unless @project.shared_versions.empty? 235 end
215 @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] } } 236 versions = @project.shared_versions.all
216 end 237 unless versions.empty?
217 unless @project.descendants.active.empty? 238 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
218 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } } 239 end
240 unless @project.leaf?
241 subprojects = @project.descendants.visible.all
242 unless subprojects.empty?
243 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
244 end
219 end 245 end
220 add_custom_fields_filters(@project.all_issue_custom_fields) 246 add_custom_fields_filters(@project.all_issue_custom_fields)
221 else 247 else
222 # global filters for cross project issue list 248 # global filters for cross project issue list
223 system_shared_versions = Version.visible.find_all_by_sharing('system') 249 system_shared_versions = Version.visible.find_all_by_sharing('system')
224 unless system_shared_versions.empty? 250 unless system_shared_versions.empty?
225 @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] } } 251 @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] } }
226 end 252 end
227 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) 253 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
228 # project filter
229 project_values = Project.all(:conditions => Project.visible_by(User.current), :order => 'lft').map do |p|
230 pre = (p.level > 0 ? ('--' * p.level + ' ') : '')
231 ["#{pre}#{p.name}",p.id.to_s]
232 end
233 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values}
234 end 254 end
235 @available_filters 255 @available_filters
236 end 256 end
237 257
238 def add_filter(field, operator, values) 258 def add_filter(field, operator, values)
239 # values must be an array 259 # values must be an array
240 return unless values and values.is_a? Array # and !values.first.empty? 260 return unless values and values.is_a? Array # and !values.first.empty?
241 # check if field is defined as an available filter 261 # check if field is defined as an available filter
242 if available_filters.has_key? field 262 if available_filters.has_key? field
247 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator 267 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
248 #end 268 #end
249 filters[field] = {:operator => operator, :values => values } 269 filters[field] = {:operator => operator, :values => values }
250 end 270 end
251 end 271 end
252 272
253 def add_short_filter(field, expression) 273 def add_short_filter(field, expression)
254 return unless expression 274 return unless expression
255 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first 275 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
256 add_filter field, (parms[0] || "="), [parms[1] || ""] 276 add_filter field, (parms[0] || "="), [parms[1] || ""]
257 end 277 end
262 fields.each do |field| 282 fields.each do |field|
263 add_filter(field, operators[field], values[field]) 283 add_filter(field, operators[field], values[field])
264 end 284 end
265 end 285 end
266 end 286 end
267 287
268 def has_filter?(field) 288 def has_filter?(field)
269 filters and filters[field] 289 filters and filters[field]
270 end 290 end
271 291
272 def operator_for(field) 292 def operator_for(field)
273 has_filter?(field) ? filters[field][:operator] : nil 293 has_filter?(field) ? filters[field][:operator] : nil
274 end 294 end
275 295
276 def values_for(field) 296 def values_for(field)
277 has_filter?(field) ? filters[field][:values] : nil 297 has_filter?(field) ? filters[field][:values] : nil
278 end 298 end
279 299
280 def label_for(field) 300 def label_for(field)
281 label = available_filters[field][:name] if available_filters.has_key?(field) 301 label = available_filters[field][:name] if available_filters.has_key?(field)
282 label ||= field.gsub(/\_id$/, "") 302 label ||= field.gsub(/\_id$/, "")
283 end 303 end
284 304
286 return @available_columns if @available_columns 306 return @available_columns if @available_columns
287 @available_columns = Query.available_columns 307 @available_columns = Query.available_columns
288 @available_columns += (project ? 308 @available_columns += (project ?
289 project.all_issue_custom_fields : 309 project.all_issue_custom_fields :
290 IssueCustomField.find(:all) 310 IssueCustomField.find(:all)
291 ).collect {|cf| QueryCustomFieldColumn.new(cf) } 311 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
292 end 312 end
293 313
294 def self.available_columns=(v) 314 def self.available_columns=(v)
295 self.available_columns = (v) 315 self.available_columns = (v)
296 end 316 end
297 317
298 def self.add_available_column(column) 318 def self.add_available_column(column)
299 self.available_columns << (column) if column.is_a?(QueryColumn) 319 self.available_columns << (column) if column.is_a?(QueryColumn)
300 end 320 end
301 321
302 # Returns an array of columns that can be used to group the results 322 # Returns an array of columns that can be used to group the results
303 def groupable_columns 323 def groupable_columns
304 available_columns.select {|c| c.groupable} 324 available_columns.select {|c| c.groupable}
305 end 325 end
306 326
309 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column| 329 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
310 h[column.name.to_s] = column.sortable 330 h[column.name.to_s] = column.sortable
311 h 331 h
312 }) 332 })
313 end 333 end
314 334
315 def columns 335 def columns
316 if has_default_columns? 336 if has_default_columns?
317 available_columns.select do |c| 337 available_columns.select do |c|
318 # Adds the project column by default for cross-project lists 338 # Adds the project column by default for cross-project lists
319 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?) 339 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
321 else 341 else
322 # preserve the column_names order 342 # preserve the column_names order
323 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact 343 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
324 end 344 end
325 end 345 end
326 346
327 def column_names=(names) 347 def column_names=(names)
328 if names 348 if names
329 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } 349 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
330 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } 350 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
331 # Set column_names to nil if default columns 351 # Set column_names to nil if default columns
333 names = nil 353 names = nil
334 end 354 end
335 end 355 end
336 write_attribute(:column_names, names) 356 write_attribute(:column_names, names)
337 end 357 end
338 358
339 def has_column?(column) 359 def has_column?(column)
340 column_names && column_names.include?(column.name) 360 column_names && column_names.include?(column.name)
341 end 361 end
342 362
343 def has_default_columns? 363 def has_default_columns?
344 column_names.nil? || column_names.empty? 364 column_names.nil? || column_names.empty?
345 end 365 end
346 366
347 def sort_criteria=(arg) 367 def sort_criteria=(arg)
348 c = [] 368 c = []
349 if arg.is_a?(Hash) 369 if arg.is_a?(Hash)
350 arg = arg.keys.sort.collect {|k| arg[k]} 370 arg = arg.keys.sort.collect {|k| arg[k]}
351 end 371 end
352 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']} 372 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
353 write_attribute(:sort_criteria, c) 373 write_attribute(:sort_criteria, c)
354 end 374 end
355 375
356 def sort_criteria 376 def sort_criteria
357 read_attribute(:sort_criteria) || [] 377 read_attribute(:sort_criteria) || []
358 end 378 end
359 379
360 def sort_criteria_key(arg) 380 def sort_criteria_key(arg)
361 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first 381 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
362 end 382 end
363 383
364 def sort_criteria_order(arg) 384 def sort_criteria_order(arg)
365 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last 385 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
366 end 386 end
367 387
368 # Returns the SQL sort order that should be prepended for grouping 388 # Returns the SQL sort order that should be prepended for grouping
369 def group_by_sort_order 389 def group_by_sort_order
370 if grouped? && (column = group_by_column) 390 if grouped? && (column = group_by_column)
371 column.sortable.is_a?(Array) ? 391 column.sortable.is_a?(Array) ?
372 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') : 392 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
373 "#{column.sortable} #{column.default_order}" 393 "#{column.sortable} #{column.default_order}"
374 end 394 end
375 end 395 end
376 396
377 # Returns true if the query is a grouped query 397 # Returns true if the query is a grouped query
378 def grouped? 398 def grouped?
379 !group_by.blank? 399 !group_by_column.nil?
380 end 400 end
381 401
382 def group_by_column 402 def group_by_column
383 groupable_columns.detect {|c| c.name.to_s == group_by} 403 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
384 end 404 end
385 405
386 def group_by_statement 406 def group_by_statement
387 group_by_column.groupable 407 group_by_column.try(:groupable)
388 end 408 end
389 409
390 def project_statement 410 def project_statement
391 project_clauses = [] 411 project_clauses = []
392 if project && !@project.descendants.active.empty? 412 if project && !@project.descendants.active.empty?
393 ids = [project.id] 413 ids = [project.id]
394 if has_filter?("subproject_id") 414 if has_filter?("subproject_id")
407 end 427 end
408 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') 428 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
409 elsif project 429 elsif project
410 project_clauses << "#{Project.table_name}.id = %d" % project.id 430 project_clauses << "#{Project.table_name}.id = %d" % project.id
411 end 431 end
412 project_clauses << Project.allowed_to_condition(User.current, :view_issues) 432 project_clauses.any? ? project_clauses.join(' AND ') : nil
413 project_clauses.join(' AND ')
414 end 433 end
415 434
416 def statement 435 def statement
417 # filters clauses 436 # filters clauses
418 filters_clauses = [] 437 filters_clauses = []
419 filters.each_key do |field| 438 filters.each_key do |field|
420 next if field == "subproject_id" 439 next if field == "subproject_id"
421 v = values_for(field).clone 440 v = values_for(field).clone
422 next unless v and !v.empty? 441 next unless v and !v.empty?
423 operator = operator_for(field) 442 operator = operator_for(field)
424 443
425 # "me" value subsitution 444 # "me" value subsitution
426 if %w(assigned_to_id author_id watcher_id).include?(field) 445 if %w(assigned_to_id author_id watcher_id).include?(field)
427 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") 446 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
428 end 447 end
429 448
430 sql = '' 449 sql = ''
431 if field =~ /^cf_(\d+)$/ 450 if field =~ /^cf_(\d+)$/
432 # custom field 451 # custom field
433 db_table = CustomValue.table_name 452 db_table = CustomValue.table_name
434 db_field = 'value' 453 db_field = 'value'
456 if group && group.user_ids.present? 475 if group && group.user_ids.present?
457 user_ids << group.user_ids 476 user_ids << group.user_ids
458 end 477 end
459 user_ids.flatten.uniq.compact 478 user_ids.flatten.uniq.compact
460 }.sort.collect(&:to_s) 479 }.sort.collect(&:to_s)
461 480
462 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')' 481 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
463 482
464 elsif field == "assigned_to_role" # named field 483 elsif field == "assigned_to_role" # named field
465 if operator == "*" # Any Role 484 if operator == "*" # Any Role
466 roles = Role.givable 485 roles = Role.givable
470 operator = '!' # Override the operator since we want to find by assigned_to 489 operator = '!' # Override the operator since we want to find by assigned_to
471 else 490 else
472 roles = Role.givable.find_all_by_id(v) 491 roles = Role.givable.find_all_by_id(v)
473 end 492 end
474 roles ||= [] 493 roles ||= []
475 494
476 members_of_roles = roles.inject([]) {|user_ids, role| 495 members_of_roles = roles.inject([]) {|user_ids, role|
477 if role && role.members 496 if role && role.members
478 user_ids << role.members.collect(&:user_id) 497 user_ids << role.members.collect(&:user_id)
479 end 498 end
480 user_ids.flatten.uniq.compact 499 user_ids.flatten.uniq.compact
481 }.sort.collect(&:to_s) 500 }.sort.collect(&:to_s)
482 501
483 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')' 502 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
484 else 503 else
485 # regular field 504 # regular field
486 db_table = Issue.table_name 505 db_table = Issue.table_name
487 db_field = field 506 db_field = field
488 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')' 507 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
489 end 508 end
490 filters_clauses << sql 509 filters_clauses << sql
491 510
492 end if filters and valid? 511 end if filters and valid?
493 512
494 (filters_clauses << project_statement).join(' AND ') 513 filters_clauses << project_statement
495 end 514 filters_clauses.reject!(&:blank?)
496 515
516 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
517 end
518
497 # Returns the issue count 519 # Returns the issue count
498 def issue_count 520 def issue_count
499 Issue.count(:include => [:status, :project], :conditions => statement) 521 Issue.count(:include => [:status, :project], :conditions => statement)
500 rescue ::ActiveRecord::StatementInvalid => e 522 rescue ::ActiveRecord::StatementInvalid => e
501 raise StatementInvalid.new(e.message) 523 raise StatementInvalid.new(e.message)
502 end 524 end
503 525
504 # Returns the issue count by group or nil if query is not grouped 526 # Returns the issue count by group or nil if query is not grouped
505 def issue_count_by_group 527 def issue_count_by_group
506 r = nil 528 r = nil
507 if grouped? 529 if grouped?
508 begin 530 begin
509 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value 531 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
510 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement) 532 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
511 rescue ActiveRecord::RecordNotFound 533 rescue ActiveRecord::RecordNotFound
512 r = {nil => issue_count} 534 r = {nil => issue_count}
513 end 535 end
514 c = group_by_column 536 c = group_by_column
515 if c.is_a?(QueryCustomFieldColumn) 537 if c.is_a?(QueryCustomFieldColumn)
518 end 540 end
519 r 541 r
520 rescue ::ActiveRecord::StatementInvalid => e 542 rescue ::ActiveRecord::StatementInvalid => e
521 raise StatementInvalid.new(e.message) 543 raise StatementInvalid.new(e.message)
522 end 544 end
523 545
524 # Returns the issues 546 # Returns the issues
525 # Valid options are :order, :offset, :limit, :include, :conditions 547 # Valid options are :order, :offset, :limit, :include, :conditions
526 def issues(options={}) 548 def issues(options={})
527 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') 549 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
528 order_option = nil if order_option.blank? 550 order_option = nil if order_option.blank?
529 551
530 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq, 552 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
531 :conditions => Query.merge_conditions(statement, options[:conditions]), 553 :conditions => Query.merge_conditions(statement, options[:conditions]),
532 :order => order_option, 554 :order => order_option,
533 :limit => options[:limit], 555 :limit => options[:limit],
534 :offset => options[:offset] 556 :offset => options[:offset]
535 rescue ::ActiveRecord::StatementInvalid => e 557 rescue ::ActiveRecord::StatementInvalid => e
537 end 559 end
538 560
539 # Returns the journals 561 # Returns the journals
540 # Valid options are :order, :offset, :limit 562 # Valid options are :order, :offset, :limit
541 def journals(options={}) 563 def journals(options={})
542 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}], 564 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
543 :conditions => statement, 565 :conditions => statement,
544 :order => options[:order], 566 :order => options[:order],
545 :limit => options[:limit], 567 :limit => options[:limit],
546 :offset => options[:offset] 568 :offset => options[:offset]
547 rescue ::ActiveRecord::StatementInvalid => e 569 rescue ::ActiveRecord::StatementInvalid => e
548 raise StatementInvalid.new(e.message) 570 raise StatementInvalid.new(e.message)
549 end 571 end
550 572
551 # Returns the versions 573 # Returns the versions
552 # Valid options are :conditions 574 # Valid options are :conditions
553 def versions(options={}) 575 def versions(options={})
554 Version.find :all, :include => :project, 576 Version.visible.find :all, :include => :project,
555 :conditions => Query.merge_conditions(project_statement, options[:conditions]) 577 :conditions => Query.merge_conditions(project_statement, options[:conditions])
556 rescue ::ActiveRecord::StatementInvalid => e 578 rescue ::ActiveRecord::StatementInvalid => e
557 raise StatementInvalid.new(e.message) 579 raise StatementInvalid.new(e.message)
558 end 580 end
559 581
560 private 582 private
561 583
562 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ 584 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
563 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) 585 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
564 sql = '' 586 sql = ''
565 case operator 587 case operator
566 when "=" 588 when "="
567 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" 589 if value.any?
590 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
591 else
592 # IN an empty set
593 sql = "1=0"
594 end
568 when "!" 595 when "!"
569 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" 596 if value.any?
597 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
598 else
599 # NOT IN an empty set
600 sql = "1=1"
601 end
570 when "!*" 602 when "!*"
571 sql = "#{db_table}.#{db_field} IS NULL" 603 sql = "#{db_table}.#{db_field} IS NULL"
572 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter 604 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
573 when "*" 605 when "*"
574 sql = "#{db_table}.#{db_field} IS NOT NULL" 606 sql = "#{db_table}.#{db_field} IS NOT NULL"
594 when "t+" 626 when "t+"
595 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i) 627 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
596 when "t" 628 when "t"
597 sql = date_range_clause(db_table, db_field, 0, 0) 629 sql = date_range_clause(db_table, db_field, 0, 0)
598 when "w" 630 when "w"
599 from = l(:general_first_day_of_week) == '7' ? 631 first_day_of_week = l(:general_first_day_of_week).to_i
600 # week starts on sunday 632 day_of_week = Date.today.cwday
601 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) : 633 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
602 # week starts on monday (Rails default) 634 sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6)
603 Time.now.at_beginning_of_week
604 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
605 when "~" 635 when "~"
606 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" 636 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
607 when "!~" 637 when "!~"
608 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" 638 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
609 end 639 end
610 640
611 return sql 641 return sql
612 end 642 end
613 643
614 def add_custom_fields_filters(custom_fields) 644 def add_custom_fields_filters(custom_fields)
615 @available_filters ||= {} 645 @available_filters ||= {}
616 646
617 custom_fields.select(&:is_filter?).each do |field| 647 custom_fields.select(&:is_filter?).each do |field|
618 case field.field_format 648 case field.field_format
619 when "text" 649 when "text"
620 options = { :type => :text, :order => 20 } 650 options = { :type => :text, :order => 20 }
621 when "list" 651 when "list"
622 options = { :type => :list_optional, :values => field.possible_values, :order => 20} 652 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
623 when "date" 653 when "date"
624 options = { :type => :date, :order => 20 } 654 options = { :type => :date, :order => 20 }
625 when "bool" 655 when "bool"
626 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } 656 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
657 when "user", "version"
658 next unless project
659 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
627 else 660 else
628 options = { :type => :string, :order => 20 } 661 options = { :type => :string, :order => 20 }
629 end 662 end
630 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) 663 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
631 end 664 end
632 end 665 end
633 666
634 # Returns a SQL clause for a date or datetime field. 667 # Returns a SQL clause for a date or datetime field.
635 def date_range_clause(table, field, from, to) 668 def date_range_clause(table, field, from, to)
636 s = [] 669 s = []
637 if from 670 if from
638 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)]) 671 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])