comparison app/models/.svn/text-base/query.rb.svn-base @ 441:cbce1fd3b1b7 redmine-1.2

Update to Redmine 1.2-stable branch (Redmine SVN rev 6000)
author Chris Cannam
date Mon, 06 Jun 2011 14:24:13 +0100
parents 051f544170fe
children 0c939c159af4
comparison
equal deleted inserted replaced
245:051f544170fe 441:cbce1fd3b1b7
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
161 def editable_by?(user) 169 def editable_by?(user)
162 return false unless user 170 return false unless user
163 # Admin can edit them all and regular users can edit their private queries 171 # 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) 172 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) 173 # 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) 174 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
167 end 175 end
168 176
169 def available_filters 177 def available_filters
170 return @available_filters if @available_filters 178 return @available_filters if @available_filters
171 179
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers 180 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
173 181
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, 182 @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] } }, 183 "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] } }, 184 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
177 "subject" => { :type => :text, :order => 8 }, 185 "subject" => { :type => :text, :order => 8 },
178 "created_on" => { :type => :date_past, :order => 9 }, 186 "created_on" => { :type => :date_past, :order => 9 },
179 "updated_on" => { :type => :date_past, :order => 10 }, 187 "updated_on" => { :type => :date_past, :order => 10 },
180 "start_date" => { :type => :date, :order => 11 }, 188 "start_date" => { :type => :date, :order => 11 },
181 "due_date" => { :type => :date, :order => 12 }, 189 "due_date" => { :type => :date, :order => 12 },
182 "estimated_hours" => { :type => :integer, :order => 13 }, 190 "estimated_hours" => { :type => :integer, :order => 13 },
183 "done_ratio" => { :type => :integer, :order => 14 }} 191 "done_ratio" => { :type => :integer, :order => 14 }}
184 192
185 user_values = [] 193 user_values = []
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? 194 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
187 if project 195 if project
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } 196 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
189 else 197 else
190 all_projects = Project.visible.all 198 all_projects = Project.visible.all
191 if all_projects.any? 199 if all_projects.any?
192 # members of visible projects 200 # 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 (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] } 201 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] }
194 202
195 # project filter 203 # project filter
196 project_values = [] 204 project_values = []
197 Project.project_tree(all_projects) do |p, level| 205 Project.project_tree(all_projects) do |p, level|
198 prefix = (level > 0 ? ('--' * level + ' ') : '') 206 prefix = (level > 0 ? ('--' * level + ' ') : '')
199 project_values << ["#{prefix}#{p.name}", p.id.to_s] 207 project_values << ["#{prefix}#{p.name}", p.id.to_s]
207 group_values = Group.all.collect {|g| [g.name, g.id.to_s] } 215 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
208 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty? 216 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
209 217
210 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } 218 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
211 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty? 219 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
212 220
213 if User.current.logged? 221 if User.current.logged?
214 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] } 222 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
215 end 223 end
216 224
217 if project 225 if project
218 # project specific filters 226 # project specific filters
219 unless @project.issue_categories.empty? 227 categories = @project.issue_categories.all
220 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } 228 unless categories.empty?
221 end 229 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
222 unless @project.shared_versions.empty? 230 end
223 @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] } } 231 versions = @project.shared_versions.all
224 end 232 unless versions.empty?
225 unless @project.descendants.active.empty? 233 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
226 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } } 234 end
235 unless @project.leaf?
236 subprojects = @project.descendants.visible.all
237 unless subprojects.empty?
238 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
239 end
227 end 240 end
228 add_custom_fields_filters(@project.all_issue_custom_fields) 241 add_custom_fields_filters(@project.all_issue_custom_fields)
229 else 242 else
230 # global filters for cross project issue list 243 # global filters for cross project issue list
231 system_shared_versions = Version.visible.find_all_by_sharing('system') 244 system_shared_versions = Version.visible.find_all_by_sharing('system')
234 end 247 end
235 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) 248 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
236 end 249 end
237 @available_filters 250 @available_filters
238 end 251 end
239 252
240 def add_filter(field, operator, values) 253 def add_filter(field, operator, values)
241 # values must be an array 254 # values must be an array
242 return unless values and values.is_a? Array # and !values.first.empty? 255 return unless values and values.is_a? Array # and !values.first.empty?
243 # check if field is defined as an available filter 256 # check if field is defined as an available filter
244 if available_filters.has_key? field 257 if available_filters.has_key? field
249 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator 262 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
250 #end 263 #end
251 filters[field] = {:operator => operator, :values => values } 264 filters[field] = {:operator => operator, :values => values }
252 end 265 end
253 end 266 end
254 267
255 def add_short_filter(field, expression) 268 def add_short_filter(field, expression)
256 return unless expression 269 return unless expression
257 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first 270 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
258 add_filter field, (parms[0] || "="), [parms[1] || ""] 271 add_filter field, (parms[0] || "="), [parms[1] || ""]
259 end 272 end
264 fields.each do |field| 277 fields.each do |field|
265 add_filter(field, operators[field], values[field]) 278 add_filter(field, operators[field], values[field])
266 end 279 end
267 end 280 end
268 end 281 end
269 282
270 def has_filter?(field) 283 def has_filter?(field)
271 filters and filters[field] 284 filters and filters[field]
272 end 285 end
273 286
274 def operator_for(field) 287 def operator_for(field)
275 has_filter?(field) ? filters[field][:operator] : nil 288 has_filter?(field) ? filters[field][:operator] : nil
276 end 289 end
277 290
278 def values_for(field) 291 def values_for(field)
279 has_filter?(field) ? filters[field][:values] : nil 292 has_filter?(field) ? filters[field][:values] : nil
280 end 293 end
281 294
282 def label_for(field) 295 def label_for(field)
283 label = available_filters[field][:name] if available_filters.has_key?(field) 296 label = available_filters[field][:name] if available_filters.has_key?(field)
284 label ||= field.gsub(/\_id$/, "") 297 label ||= field.gsub(/\_id$/, "")
285 end 298 end
286 299
288 return @available_columns if @available_columns 301 return @available_columns if @available_columns
289 @available_columns = Query.available_columns 302 @available_columns = Query.available_columns
290 @available_columns += (project ? 303 @available_columns += (project ?
291 project.all_issue_custom_fields : 304 project.all_issue_custom_fields :
292 IssueCustomField.find(:all) 305 IssueCustomField.find(:all)
293 ).collect {|cf| QueryCustomFieldColumn.new(cf) } 306 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
294 end 307 end
295 308
296 def self.available_columns=(v) 309 def self.available_columns=(v)
297 self.available_columns = (v) 310 self.available_columns = (v)
298 end 311 end
299 312
300 def self.add_available_column(column) 313 def self.add_available_column(column)
301 self.available_columns << (column) if column.is_a?(QueryColumn) 314 self.available_columns << (column) if column.is_a?(QueryColumn)
302 end 315 end
303 316
304 # Returns an array of columns that can be used to group the results 317 # Returns an array of columns that can be used to group the results
305 def groupable_columns 318 def groupable_columns
306 available_columns.select {|c| c.groupable} 319 available_columns.select {|c| c.groupable}
307 end 320 end
308 321
311 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column| 324 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
312 h[column.name.to_s] = column.sortable 325 h[column.name.to_s] = column.sortable
313 h 326 h
314 }) 327 })
315 end 328 end
316 329
317 def columns 330 def columns
318 if has_default_columns? 331 if has_default_columns?
319 available_columns.select do |c| 332 available_columns.select do |c|
320 # Adds the project column by default for cross-project lists 333 # Adds the project column by default for cross-project lists
321 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?) 334 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
323 else 336 else
324 # preserve the column_names order 337 # preserve the column_names order
325 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact 338 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
326 end 339 end
327 end 340 end
328 341
329 def column_names=(names) 342 def column_names=(names)
330 if names 343 if names
331 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } 344 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
332 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } 345 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
333 # Set column_names to nil if default columns 346 # Set column_names to nil if default columns
335 names = nil 348 names = nil
336 end 349 end
337 end 350 end
338 write_attribute(:column_names, names) 351 write_attribute(:column_names, names)
339 end 352 end
340 353
341 def has_column?(column) 354 def has_column?(column)
342 column_names && column_names.include?(column.name) 355 column_names && column_names.include?(column.name)
343 end 356 end
344 357
345 def has_default_columns? 358 def has_default_columns?
346 column_names.nil? || column_names.empty? 359 column_names.nil? || column_names.empty?
347 end 360 end
348 361
349 def sort_criteria=(arg) 362 def sort_criteria=(arg)
350 c = [] 363 c = []
351 if arg.is_a?(Hash) 364 if arg.is_a?(Hash)
352 arg = arg.keys.sort.collect {|k| arg[k]} 365 arg = arg.keys.sort.collect {|k| arg[k]}
353 end 366 end
354 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']} 367 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
355 write_attribute(:sort_criteria, c) 368 write_attribute(:sort_criteria, c)
356 end 369 end
357 370
358 def sort_criteria 371 def sort_criteria
359 read_attribute(:sort_criteria) || [] 372 read_attribute(:sort_criteria) || []
360 end 373 end
361 374
362 def sort_criteria_key(arg) 375 def sort_criteria_key(arg)
363 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first 376 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
364 end 377 end
365 378
366 def sort_criteria_order(arg) 379 def sort_criteria_order(arg)
367 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last 380 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
368 end 381 end
369 382
370 # Returns the SQL sort order that should be prepended for grouping 383 # Returns the SQL sort order that should be prepended for grouping
371 def group_by_sort_order 384 def group_by_sort_order
372 if grouped? && (column = group_by_column) 385 if grouped? && (column = group_by_column)
373 column.sortable.is_a?(Array) ? 386 column.sortable.is_a?(Array) ?
374 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') : 387 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
375 "#{column.sortable} #{column.default_order}" 388 "#{column.sortable} #{column.default_order}"
376 end 389 end
377 end 390 end
378 391
379 # Returns true if the query is a grouped query 392 # Returns true if the query is a grouped query
380 def grouped? 393 def grouped?
381 !group_by_column.nil? 394 !group_by_column.nil?
382 end 395 end
383 396
384 def group_by_column 397 def group_by_column
385 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by} 398 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
386 end 399 end
387 400
388 def group_by_statement 401 def group_by_statement
389 group_by_column.try(:groupable) 402 group_by_column.try(:groupable)
390 end 403 end
391 404
392 def project_statement 405 def project_statement
393 project_clauses = [] 406 project_clauses = []
394 if project && !@project.descendants.active.empty? 407 if project && !@project.descendants.active.empty?
395 ids = [project.id] 408 ids = [project.id]
396 if has_filter?("subproject_id") 409 if has_filter?("subproject_id")
409 end 422 end
410 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') 423 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
411 elsif project 424 elsif project
412 project_clauses << "#{Project.table_name}.id = %d" % project.id 425 project_clauses << "#{Project.table_name}.id = %d" % project.id
413 end 426 end
414 project_clauses << Project.allowed_to_condition(User.current, :view_issues) 427 project_clauses.any? ? project_clauses.join(' AND ') : nil
415 project_clauses.join(' AND ')
416 end 428 end
417 429
418 def statement 430 def statement
419 # filters clauses 431 # filters clauses
420 filters_clauses = [] 432 filters_clauses = []
421 filters.each_key do |field| 433 filters.each_key do |field|
422 next if field == "subproject_id" 434 next if field == "subproject_id"
423 v = values_for(field).clone 435 v = values_for(field).clone
424 next unless v and !v.empty? 436 next unless v and !v.empty?
425 operator = operator_for(field) 437 operator = operator_for(field)
426 438
427 # "me" value subsitution 439 # "me" value subsitution
428 if %w(assigned_to_id author_id watcher_id).include?(field) 440 if %w(assigned_to_id author_id watcher_id).include?(field)
429 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") 441 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
430 end 442 end
431 443
432 sql = '' 444 sql = ''
433 if field =~ /^cf_(\d+)$/ 445 if field =~ /^cf_(\d+)$/
434 # custom field 446 # custom field
435 db_table = CustomValue.table_name 447 db_table = CustomValue.table_name
436 db_field = 'value' 448 db_field = 'value'
458 if group && group.user_ids.present? 470 if group && group.user_ids.present?
459 user_ids << group.user_ids 471 user_ids << group.user_ids
460 end 472 end
461 user_ids.flatten.uniq.compact 473 user_ids.flatten.uniq.compact
462 }.sort.collect(&:to_s) 474 }.sort.collect(&:to_s)
463 475
464 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')' 476 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
465 477
466 elsif field == "assigned_to_role" # named field 478 elsif field == "assigned_to_role" # named field
467 if operator == "*" # Any Role 479 if operator == "*" # Any Role
468 roles = Role.givable 480 roles = Role.givable
472 operator = '!' # Override the operator since we want to find by assigned_to 484 operator = '!' # Override the operator since we want to find by assigned_to
473 else 485 else
474 roles = Role.givable.find_all_by_id(v) 486 roles = Role.givable.find_all_by_id(v)
475 end 487 end
476 roles ||= [] 488 roles ||= []
477 489
478 members_of_roles = roles.inject([]) {|user_ids, role| 490 members_of_roles = roles.inject([]) {|user_ids, role|
479 if role && role.members 491 if role && role.members
480 user_ids << role.members.collect(&:user_id) 492 user_ids << role.members.collect(&:user_id)
481 end 493 end
482 user_ids.flatten.uniq.compact 494 user_ids.flatten.uniq.compact
483 }.sort.collect(&:to_s) 495 }.sort.collect(&:to_s)
484 496
485 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')' 497 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
486 else 498 else
487 # regular field 499 # regular field
488 db_table = Issue.table_name 500 db_table = Issue.table_name
489 db_field = field 501 db_field = field
490 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')' 502 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
491 end 503 end
492 filters_clauses << sql 504 filters_clauses << sql
493 505
494 end if filters and valid? 506 end if filters and valid?
495 507
496 (filters_clauses << project_statement).join(' AND ') 508 filters_clauses << project_statement
497 end 509 filters_clauses.reject!(&:blank?)
498 510
511 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
512 end
513
499 # Returns the issue count 514 # Returns the issue count
500 def issue_count 515 def issue_count
501 Issue.count(:include => [:status, :project], :conditions => statement) 516 Issue.count(:include => [:status, :project], :conditions => statement)
502 rescue ::ActiveRecord::StatementInvalid => e 517 rescue ::ActiveRecord::StatementInvalid => e
503 raise StatementInvalid.new(e.message) 518 raise StatementInvalid.new(e.message)
504 end 519 end
505 520
506 # Returns the issue count by group or nil if query is not grouped 521 # Returns the issue count by group or nil if query is not grouped
507 def issue_count_by_group 522 def issue_count_by_group
508 r = nil 523 r = nil
509 if grouped? 524 if grouped?
510 begin 525 begin
511 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value 526 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
512 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement) 527 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
513 rescue ActiveRecord::RecordNotFound 528 rescue ActiveRecord::RecordNotFound
514 r = {nil => issue_count} 529 r = {nil => issue_count}
515 end 530 end
516 c = group_by_column 531 c = group_by_column
517 if c.is_a?(QueryCustomFieldColumn) 532 if c.is_a?(QueryCustomFieldColumn)
520 end 535 end
521 r 536 r
522 rescue ::ActiveRecord::StatementInvalid => e 537 rescue ::ActiveRecord::StatementInvalid => e
523 raise StatementInvalid.new(e.message) 538 raise StatementInvalid.new(e.message)
524 end 539 end
525 540
526 # Returns the issues 541 # Returns the issues
527 # Valid options are :order, :offset, :limit, :include, :conditions 542 # Valid options are :order, :offset, :limit, :include, :conditions
528 def issues(options={}) 543 def issues(options={})
529 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') 544 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
530 order_option = nil if order_option.blank? 545 order_option = nil if order_option.blank?
531 546
532 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq, 547 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
533 :conditions => Query.merge_conditions(statement, options[:conditions]), 548 :conditions => Query.merge_conditions(statement, options[:conditions]),
534 :order => order_option, 549 :order => order_option,
535 :limit => options[:limit], 550 :limit => options[:limit],
536 :offset => options[:offset] 551 :offset => options[:offset]
537 rescue ::ActiveRecord::StatementInvalid => e 552 rescue ::ActiveRecord::StatementInvalid => e
539 end 554 end
540 555
541 # Returns the journals 556 # Returns the journals
542 # Valid options are :order, :offset, :limit 557 # Valid options are :order, :offset, :limit
543 def journals(options={}) 558 def journals(options={})
544 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}], 559 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
545 :conditions => statement, 560 :conditions => statement,
546 :order => options[:order], 561 :order => options[:order],
547 :limit => options[:limit], 562 :limit => options[:limit],
548 :offset => options[:offset] 563 :offset => options[:offset]
549 rescue ::ActiveRecord::StatementInvalid => e 564 rescue ::ActiveRecord::StatementInvalid => e
550 raise StatementInvalid.new(e.message) 565 raise StatementInvalid.new(e.message)
551 end 566 end
552 567
553 # Returns the versions 568 # Returns the versions
554 # Valid options are :conditions 569 # Valid options are :conditions
555 def versions(options={}) 570 def versions(options={})
556 Version.find :all, :include => :project, 571 Version.visible.find :all, :include => :project,
557 :conditions => Query.merge_conditions(project_statement, options[:conditions]) 572 :conditions => Query.merge_conditions(project_statement, options[:conditions])
558 rescue ::ActiveRecord::StatementInvalid => e 573 rescue ::ActiveRecord::StatementInvalid => e
559 raise StatementInvalid.new(e.message) 574 raise StatementInvalid.new(e.message)
560 end 575 end
561 576
562 private 577 private
563 578
564 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ 579 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
565 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) 580 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
566 sql = '' 581 sql = ''
567 case operator 582 case operator
568 when "=" 583 when "="
606 when "t+" 621 when "t+"
607 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i) 622 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
608 when "t" 623 when "t"
609 sql = date_range_clause(db_table, db_field, 0, 0) 624 sql = date_range_clause(db_table, db_field, 0, 0)
610 when "w" 625 when "w"
611 from = l(:general_first_day_of_week) == '7' ? 626 first_day_of_week = l(:general_first_day_of_week).to_i
612 # week starts on sunday 627 day_of_week = Date.today.cwday
613 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) : 628 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
614 # week starts on monday (Rails default) 629 sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6)
615 Time.now.at_beginning_of_week
616 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
617 when "~" 630 when "~"
618 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" 631 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
619 when "!~" 632 when "!~"
620 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" 633 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
621 end 634 end
622 635
623 return sql 636 return sql
624 end 637 end
625 638
626 def add_custom_fields_filters(custom_fields) 639 def add_custom_fields_filters(custom_fields)
627 @available_filters ||= {} 640 @available_filters ||= {}
628 641
629 custom_fields.select(&:is_filter?).each do |field| 642 custom_fields.select(&:is_filter?).each do |field|
630 case field.field_format 643 case field.field_format
631 when "text" 644 when "text"
632 options = { :type => :text, :order => 20 } 645 options = { :type => :text, :order => 20 }
633 when "list" 646 when "list"
634 options = { :type => :list_optional, :values => field.possible_values, :order => 20} 647 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
635 when "date" 648 when "date"
636 options = { :type => :date, :order => 20 } 649 options = { :type => :date, :order => 20 }
637 when "bool" 650 when "bool"
638 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } 651 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
652 when "user", "version"
653 next unless project
654 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
639 else 655 else
640 options = { :type => :string, :order => 20 } 656 options = { :type => :string, :order => 20 }
641 end 657 end
642 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) 658 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
643 end 659 end
644 end 660 end
645 661
646 # Returns a SQL clause for a date or datetime field. 662 # Returns a SQL clause for a date or datetime field.
647 def date_range_clause(table, field, from, to) 663 def date_range_clause(table, field, from, to)
648 s = [] 664 s = []
649 if from 665 if from
650 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)]) 666 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])