comparison app/models/query.rb @ 1338:25603efa57b5

Merge from live branch
author Chris Cannam
date Thu, 20 Jun 2013 13:14:14 +0100
parents 433d4f72a19b
children 622f24f53b42 261b3d9a4903
comparison
equal deleted inserted replaced
1209:1b1138f6f55e 1338:25603efa57b5
1 # Redmine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang 2 # Copyright (C) 2006-2012 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.
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 @inline = options.key?(:inline) ? options[:inline] : true
30 @caption_key = options[:caption] || "field_#{name}" 31 @caption_key = options[:caption] || "field_#{name}"
31 end 32 end
32 33
33 def caption 34 def caption
34 l(@caption_key) 35 l(@caption_key)
36 37
37 # Returns true if the column is sortable, otherwise false 38 # Returns true if the column is sortable, otherwise false
38 def sortable? 39 def sortable?
39 !@sortable.nil? 40 !@sortable.nil?
40 end 41 end
41 42
42 def sortable 43 def sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable 44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
45 end
46
47 def inline?
48 @inline
44 end 49 end
45 50
46 def value(issue) 51 def value(issue)
47 issue.send name 52 issue.send name
48 end 53 end
55 class QueryCustomFieldColumn < QueryColumn 60 class QueryCustomFieldColumn < QueryColumn
56 61
57 def initialize(custom_field) 62 def initialize(custom_field)
58 self.name = "cf_#{custom_field.id}".to_sym 63 self.name = "cf_#{custom_field.id}".to_sym
59 self.sortable = custom_field.order_statement || false 64 self.sortable = custom_field.order_statement || false
60 if %w(list date bool int).include?(custom_field.field_format) 65 self.groupable = custom_field.group_statement || false
61 self.groupable = custom_field.order_statement 66 @inline = true
62 end
63 self.groupable ||= false
64 @cf = custom_field 67 @cf = custom_field
65 end 68 end
66 69
67 def caption 70 def caption
68 @cf.name 71 @cf.name
71 def custom_field 74 def custom_field
72 @cf 75 @cf
73 end 76 end
74 77
75 def value(issue) 78 def value(issue)
76 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id} 79 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
77 cv && @cf.cast_value(cv.value) 80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
78 end 81 end
79 82
80 def css_classes 83 def css_classes
81 @css_classes ||= "#{name} #{@cf.field_format}" 84 @css_classes ||= "#{name} #{@cf.field_format}"
82 end 85 end
92 serialize :column_names 95 serialize :column_names
93 serialize :sort_criteria, Array 96 serialize :sort_criteria, Array
94 97
95 attr_protected :project_id, :user_id 98 attr_protected :project_id, :user_id
96 99
97 validates_presence_of :name, :on => :save 100 validates_presence_of :name
98 validates_length_of :name, :maximum => 255 101 validates_length_of :name, :maximum => 255
99 validate :validate_query_filters 102 validate :validate_query_filters
100 103
101 @@operators = { "=" => :label_equals, 104 @@operators = { "=" => :label_equals,
102 "!" => :label_not_equals, 105 "!" => :label_not_equals,
103 "o" => :label_open_issues, 106 "o" => :label_open_issues,
104 "c" => :label_closed_issues, 107 "c" => :label_closed_issues,
105 "!*" => :label_none, 108 "!*" => :label_none,
106 "*" => :label_all, 109 "*" => :label_any,
107 ">=" => :label_greater_or_equal, 110 ">=" => :label_greater_or_equal,
108 "<=" => :label_less_or_equal, 111 "<=" => :label_less_or_equal,
109 "><" => :label_between, 112 "><" => :label_between,
110 "<t+" => :label_in_less_than, 113 "<t+" => :label_in_less_than,
111 ">t+" => :label_in_more_than, 114 ">t+" => :label_in_more_than,
115 "><t+"=> :label_in_the_next_days,
112 "t+" => :label_in, 116 "t+" => :label_in,
113 "t" => :label_today, 117 "t" => :label_today,
114 "w" => :label_this_week, 118 "w" => :label_this_week,
115 ">t-" => :label_less_than_ago, 119 ">t-" => :label_less_than_ago,
116 "<t-" => :label_more_than_ago, 120 "<t-" => :label_more_than_ago,
121 "><t-"=> :label_in_the_past_days,
117 "t-" => :label_ago, 122 "t-" => :label_ago,
118 "~" => :label_contains, 123 "~" => :label_contains,
119 "!~" => :label_not_contains } 124 "!~" => :label_not_contains,
125 "=p" => :label_any_issues_in_project,
126 "=!p" => :label_any_issues_not_in_project,
127 "!p" => :label_no_issues_in_project}
120 128
121 cattr_reader :operators 129 cattr_reader :operators
122 130
123 @@operators_by_filter_type = { :list => [ "=", "!" ], 131 @@operators_by_filter_type = { :list => [ "=", "!" ],
124 :list_status => [ "o", "=", "!", "c", "*" ], 132 :list_status => [ "o", "=", "!", "c", "*" ],
125 :list_optional => [ "=", "!", "!*", "*" ], 133 :list_optional => [ "=", "!", "!*", "*" ],
126 :list_subprojects => [ "*", "!*", "=" ], 134 :list_subprojects => [ "*", "!*", "=" ],
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ], 135 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ], 136 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
129 :string => [ "=", "~", "!", "!~" ], 137 :string => [ "=", "~", "!", "!~", "!*", "*" ],
130 :text => [ "~", "!~" ], 138 :text => [ "~", "!~", "!*", "*" ],
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ], 139 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] } 140 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
141 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
133 142
134 cattr_reader :operators_by_filter_type 143 cattr_reader :operators_by_filter_type
135 144
136 @@available_columns = [ 145 @@available_columns = [
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), 146 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), 151 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true), 152 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true), 153 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), 154 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), 155 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true), 156 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), 157 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), 158 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), 159 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), 160 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), 161 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
162 QueryColumn.new(:relations, :caption => :label_related_issues),
163 QueryColumn.new(:description, :inline => false)
153 ] 164 ]
154 cattr_reader :available_columns 165 cattr_reader :available_columns
155 166
156 named_scope :visible, lambda {|*args| 167 scope :visible, lambda {|*args|
157 user = args.shift || User.current 168 user = args.shift || User.current
158 base = Project.allowed_to_condition(user, :view_issues, *args) 169 base = Project.allowed_to_condition(user, :view_issues, *args)
159 user_id = user.logged? ? user.id : 0 170 user_id = user.logged? ? user.id : 0
160 { 171 {
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id], 172 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
162 :include => :project 173 :include => :project
163 } 174 }
164 } 175 }
165 176
166 def initialize(attributes = nil) 177 def initialize(attributes=nil, *args)
167 super attributes 178 super attributes
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } 179 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
169 end
170
171 def after_initialize
172 # Store the fact that project is nil (used in #editable_by?)
173 @is_for_all = project.nil? 180 @is_for_all = project.nil?
174 end 181 end
175 182
176 def validate_query_filters 183 def validate_query_filters
177 filters.each_key do |field| 184 filters.each_key do |field|
178 if values_for(field) 185 if values_for(field)
179 case type_for(field) 186 case type_for(field)
180 when :integer 187 when :integer
181 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) } 188 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
182 when :float 189 when :float
183 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) } 190 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
184 when :date, :date_past 191 when :date, :date_past
185 case operator_for(field) 192 case operator_for(field)
186 when "=", ">=", "<=", "><" 193 when "=", ">=", "<=", "><"
187 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) } 194 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
188 when ">t-", "<t-", "t-" 195 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
189 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) } 196 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
190 end 197 end
191 end 198 end
192 end 199 end
193 200
194 errors.add label_for(field), :blank unless 201 add_filter_error(field, :blank) unless
195 # filter requires one or more values 202 # filter requires one or more values
196 (values_for(field) and !values_for(field).first.blank?) or 203 (values_for(field) and !values_for(field).first.blank?) or
197 # filter doesn't require any value 204 # filter doesn't require any value
198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field) 205 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
199 end if filters 206 end if filters
207 end
208
209 def add_filter_error(field, message)
210 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
211 errors.add(:base, m)
200 end 212 end
201 213
202 # Returns true if the query is visible to +user+ or the current user. 214 # Returns true if the query is visible to +user+ or the current user.
203 def visible?(user=User.current) 215 def visible?(user=User.current)
204 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id) 216 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
210 return true if user.admin? || (!is_public && self.user_id == user.id) 222 return true if user.admin? || (!is_public && self.user_id == user.id)
211 # Members can not edit public queries that are for all project (only admin is allowed to) 223 # Members can not edit public queries that are for all project (only admin is allowed to)
212 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project) 224 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
213 end 225 end
214 226
227 def trackers
228 @trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
229 end
230
231 # Returns a hash of localized labels for all filter operators
232 def self.operators_labels
233 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
234 end
235
215 def available_filters 236 def available_filters
216 return @available_filters if @available_filters 237 return @available_filters if @available_filters
217 238 @available_filters = {
218 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers 239 "status_id" => {
219 240 :type => :list_status, :order => 0,
220 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, 241 :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] }
221 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } }, 242 },
222 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } }, 243 "tracker_id" => {
223 "subject" => { :type => :text, :order => 8 }, 244 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
224 "created_on" => { :type => :date_past, :order => 9 }, 245 },
225 "updated_on" => { :type => :date_past, :order => 10 }, 246 "priority_id" => {
226 "start_date" => { :type => :date, :order => 11 }, 247 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
227 "due_date" => { :type => :date, :order => 12 }, 248 },
228 "estimated_hours" => { :type => :float, :order => 13 }, 249 "subject" => { :type => :text, :order => 8 },
229 "done_ratio" => { :type => :integer, :order => 14 }} 250 "created_on" => { :type => :date_past, :order => 9 },
230 251 "updated_on" => { :type => :date_past, :order => 10 },
252 "start_date" => { :type => :date, :order => 11 },
253 "due_date" => { :type => :date, :order => 12 },
254 "estimated_hours" => { :type => :float, :order => 13 },
255 "done_ratio" => { :type => :integer, :order => 14 }
256 }
257 IssueRelation::TYPES.each do |relation_type, options|
258 @available_filters[relation_type] = {
259 :type => :relation, :order => @available_filters.size + 100,
260 :label => options[:name]
261 }
262 end
231 principals = [] 263 principals = []
232 if project 264 if project
233 principals += project.principals.sort 265 principals += project.principals.sort
266 unless project.leaf?
267 subprojects = project.descendants.visible.all
268 if subprojects.any?
269 @available_filters["subproject_id"] = {
270 :type => :list_subprojects, :order => 13,
271 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
272 }
273 principals += Principal.member_of(subprojects)
274 end
275 end
234 else 276 else
235 all_projects = Project.visible.all
236 if all_projects.any? 277 if all_projects.any?
237 # members of visible projects 278 # members of visible projects
238 principals += Principal.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort 279 principals += Principal.member_of(all_projects)
239
240 # project filter 280 # project filter
241 project_values = [] 281 project_values = []
242 Project.project_tree(all_projects) do |p, level| 282 if User.current.logged? && User.current.memberships.any?
243 prefix = (level > 0 ? ('--' * level + ' ') : '') 283 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
244 project_values << ["#{prefix}#{p.name}", p.id.to_s] 284 end
245 end 285 project_values += all_projects_values
246 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty? 286 @available_filters["project_id"] = {
247 end 287 :type => :list, :order => 1, :values => project_values
248 end 288 } unless project_values.empty?
289 end
290 end
291 principals.uniq!
292 principals.sort!
249 users = principals.select {|p| p.is_a?(User)} 293 users = principals.select {|p| p.is_a?(User)}
250 294
251 assigned_to_values = [] 295 assigned_to_values = []
252 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? 296 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
253 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] } 297 assigned_to_values += (Setting.issue_group_assignment? ?
254 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty? 298 principals : users).collect{|s| [s.name, s.id.to_s] }
299 @available_filters["assigned_to_id"] = {
300 :type => :list_optional, :order => 4, :values => assigned_to_values
301 } unless assigned_to_values.empty?
255 302
256 author_values = [] 303 author_values = []
257 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? 304 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
258 author_values += users.collect{|s| [s.name, s.id.to_s] } 305 author_values += users.collect{|s| [s.name, s.id.to_s] }
259 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty? 306 @available_filters["author_id"] = {
307 :type => :list, :order => 5, :values => author_values
308 } unless author_values.empty?
260 309
261 group_values = Group.all.collect {|g| [g.name, g.id.to_s] } 310 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
262 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty? 311 @available_filters["member_of_group"] = {
312 :type => :list_optional, :order => 6, :values => group_values
313 } unless group_values.empty?
263 314
264 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } 315 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
265 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty? 316 @available_filters["assigned_to_role"] = {
317 :type => :list_optional, :order => 7, :values => role_values
318 } unless role_values.empty?
266 319
267 if User.current.logged? 320 if User.current.logged?
268 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] } 321 @available_filters["watcher_id"] = {
322 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
323 }
269 end 324 end
270 325
271 if project 326 if project
272 # project specific filters 327 # project specific filters
273 categories = project.issue_categories.all 328 categories = project.issue_categories.all
274 unless categories.empty? 329 unless categories.empty?
275 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } } 330 @available_filters["category_id"] = {
331 :type => :list_optional, :order => 6,
332 :values => categories.collect{|s| [s.name, s.id.to_s] }
333 }
276 end 334 end
277 versions = project.shared_versions.all 335 versions = project.shared_versions.all
278 unless versions.empty? 336 unless versions.empty?
279 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } } 337 @available_filters["fixed_version_id"] = {
280 end 338 :type => :list_optional, :order => 7,
281 unless project.leaf? 339 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
282 subprojects = project.descendants.visible.all 340 }
283 unless subprojects.empty?
284 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
285 end
286 end 341 end
287 add_custom_fields_filters(project.all_issue_custom_fields) 342 add_custom_fields_filters(project.all_issue_custom_fields)
288 else 343 else
289 # global filters for cross project issue list 344 # global filters for cross project issue list
290 system_shared_versions = Version.visible.find_all_by_sharing('system') 345 system_shared_versions = Version.visible.find_all_by_sharing('system')
291 unless system_shared_versions.empty? 346 unless system_shared_versions.empty?
292 @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] } } 347 @available_filters["fixed_version_id"] = {
293 end 348 :type => :list_optional, :order => 7,
294 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) 349 :values => system_shared_versions.sort.collect{|s|
350 ["#{s.project.name} - #{s.name}", s.id.to_s]
351 }
352 }
353 end
354 add_custom_fields_filters(
355 IssueCustomField.find(:all,
356 :conditions => {
357 :is_filter => true,
358 :is_for_all => true
359 }))
360 end
361 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
362 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
363 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
364 @available_filters["is_private"] = {
365 :type => :list, :order => 16,
366 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
367 }
368 end
369 Tracker.disabled_core_fields(trackers).each {|field|
370 @available_filters.delete field
371 }
372 @available_filters.each do |field, options|
373 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
295 end 374 end
296 @available_filters 375 @available_filters
376 end
377
378 # Returns a representation of the available filters for JSON serialization
379 def available_filters_as_json
380 json = {}
381 available_filters.each do |field, options|
382 json[field] = options.slice(:type, :name, :values).stringify_keys
383 end
384 json
385 end
386
387 def all_projects
388 @all_projects ||= Project.visible.all
389 end
390
391 def all_projects_values
392 return @all_projects_values if @all_projects_values
393
394 values = []
395 Project.project_tree(all_projects) do |p, level|
396 prefix = (level > 0 ? ('--' * level + ' ') : '')
397 values << ["#{prefix}#{p.name}", p.id.to_s]
398 end
399 @all_projects_values = values
297 end 400 end
298 401
299 def add_filter(field, operator, values) 402 def add_filter(field, operator, values)
300 # values must be an array 403 # values must be an array
301 return unless values.nil? || values.is_a?(Array) 404 return unless values.nil? || values.is_a?(Array)
349 (values_for(field) || [])[index] 452 (values_for(field) || [])[index]
350 end 453 end
351 454
352 def label_for(field) 455 def label_for(field)
353 label = available_filters[field][:name] if available_filters.has_key?(field) 456 label = available_filters[field][:name] if available_filters.has_key?(field)
354 label ||= field.gsub(/\_id$/, "") 457 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
355 end 458 end
356 459
357 def available_columns 460 def available_columns
358 return @available_columns if @available_columns 461 return @available_columns if @available_columns
359 @available_columns = ::Query.available_columns 462 @available_columns = ::Query.available_columns.dup
360 @available_columns += (project ? 463 @available_columns += (project ?
361 project.all_issue_custom_fields : 464 project.all_issue_custom_fields :
362 IssueCustomField.find(:all) 465 IssueCustomField.find(:all)
363 ).collect {|cf| QueryCustomFieldColumn.new(cf) } 466 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
467
468 if User.current.allowed_to?(:view_time_entries, project, :global => true)
469 index = nil
470 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
471 index = (index ? index + 1 : -1)
472 # insert the column after estimated_hours or at the end
473 @available_columns.insert index, QueryColumn.new(:spent_hours,
474 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
475 :default_order => 'desc',
476 :caption => :label_spent_time
477 )
478 end
479
480 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
481 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
482 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
483 end
484
485 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
486 @available_columns.reject! {|column|
487 disabled_fields.include?(column.name.to_s)
488 }
489
490 @available_columns
364 end 491 end
365 492
366 def self.available_columns=(v) 493 def self.available_columns=(v)
367 self.available_columns = (v) 494 self.available_columns = (v)
368 end 495 end
387 def columns 514 def columns
388 # preserve the column_names order 515 # preserve the column_names order
389 (has_default_columns? ? default_columns_names : column_names).collect do |name| 516 (has_default_columns? ? default_columns_names : column_names).collect do |name|
390 available_columns.find { |col| col.name == name } 517 available_columns.find { |col| col.name == name }
391 end.compact 518 end.compact
519 end
520
521 def inline_columns
522 columns.select(&:inline?)
523 end
524
525 def block_columns
526 columns.reject(&:inline?)
527 end
528
529 def available_inline_columns
530 available_columns.select(&:inline?)
531 end
532
533 def available_block_columns
534 available_columns.reject(&:inline?)
392 end 535 end
393 536
394 def default_columns_names 537 def default_columns_names
395 @default_columns_names ||= begin 538 @default_columns_names ||= begin
396 default_columns = Setting.issue_list_default_columns.map(&:to_sym) 539 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
410 end 553 end
411 write_attribute(:column_names, names) 554 write_attribute(:column_names, names)
412 end 555 end
413 556
414 def has_column?(column) 557 def has_column?(column)
415 column_names && column_names.include?(column.name) 558 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
416 end 559 end
417 560
418 def has_default_columns? 561 def has_default_columns?
419 column_names.nil? || column_names.empty? 562 column_names.nil? || column_names.empty?
420 end 563 end
422 def sort_criteria=(arg) 565 def sort_criteria=(arg)
423 c = [] 566 c = []
424 if arg.is_a?(Hash) 567 if arg.is_a?(Hash)
425 arg = arg.keys.sort.collect {|k| arg[k]} 568 arg = arg.keys.sort.collect {|k| arg[k]}
426 end 569 end
427 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']} 570 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
428 write_attribute(:sort_criteria, c) 571 write_attribute(:sort_criteria, c)
429 end 572 end
430 573
431 def sort_criteria 574 def sort_criteria
432 read_attribute(:sort_criteria) || [] 575 read_attribute(:sort_criteria) || []
436 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first 579 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
437 end 580 end
438 581
439 def sort_criteria_order(arg) 582 def sort_criteria_order(arg)
440 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last 583 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
584 end
585
586 def sort_criteria_order_for(key)
587 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
441 end 588 end
442 589
443 # Returns the SQL sort order that should be prepended for grouping 590 # Returns the SQL sort order that should be prepended for grouping
444 def group_by_sort_order 591 def group_by_sort_order
445 if grouped? && (column = group_by_column) 592 if grouped? && (column = group_by_column)
593 order = sort_criteria_order_for(column.name) || column.default_order
446 column.sortable.is_a?(Array) ? 594 column.sortable.is_a?(Array) ?
447 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') : 595 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
448 "#{column.sortable} #{column.default_order}" 596 "#{column.sortable} #{order}"
449 end 597 end
450 end 598 end
451 599
452 # Returns true if the query is a grouped query 600 # Returns true if the query is a grouped query
453 def grouped? 601 def grouped?
506 v.push("0") 654 v.push("0")
507 end 655 end
508 end 656 end
509 end 657 end
510 658
511 if field =~ /^cf_(\d+)$/ 659 if field == 'project_id'
660 if v.delete('mine')
661 v += User.current.memberships.map(&:project_id).map(&:to_s)
662 end
663 end
664
665 if field =~ /cf_(\d+)$/
512 # custom field 666 # custom field
513 filters_clauses << sql_for_custom_field(field, operator, v, $1) 667 filters_clauses << sql_for_custom_field(field, operator, v, $1)
514 elsif respond_to?("sql_for_#{field}_field") 668 elsif respond_to?("sql_for_#{field}_field")
515 # specific statement 669 # specific statement
516 filters_clauses << send("sql_for_#{field}_field", field, operator, v) 670 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
536 # Returns the issue count by group or nil if query is not grouped 690 # Returns the issue count by group or nil if query is not grouped
537 def issue_count_by_group 691 def issue_count_by_group
538 r = nil 692 r = nil
539 if grouped? 693 if grouped?
540 begin 694 begin
541 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value 695 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
542 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement) 696 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
543 rescue ActiveRecord::RecordNotFound 697 rescue ActiveRecord::RecordNotFound
544 r = {nil => issue_count} 698 r = {nil => issue_count}
545 end 699 end
546 c = group_by_column 700 c = group_by_column
556 # Returns the issues 710 # Returns the issues
557 # Valid options are :order, :offset, :limit, :include, :conditions 711 # Valid options are :order, :offset, :limit, :include, :conditions
558 def issues(options={}) 712 def issues(options={})
559 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') 713 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
560 order_option = nil if order_option.blank? 714 order_option = nil if order_option.blank?
561 715
562 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil 716 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
563
564 Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
565 :conditions => statement, 717 :conditions => statement,
566 :order => order_option, 718 :order => order_option,
567 :joins => joins, 719 :joins => joins_for_order_statement(order_option),
568 :limit => options[:limit], 720 :limit => options[:limit],
569 :offset => options[:offset] 721 :offset => options[:offset]
722
723 if has_column?(:spent_hours)
724 Issue.load_visible_spent_hours(issues)
725 end
726 if has_column?(:relations)
727 Issue.load_visible_relations(issues)
728 end
729 issues
730 rescue ::ActiveRecord::StatementInvalid => e
731 raise StatementInvalid.new(e.message)
732 end
733
734 # Returns the issues ids
735 def issue_ids(options={})
736 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
737 order_option = nil if order_option.blank?
738
739 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
740 :conditions => statement,
741 :order => order_option,
742 :joins => joins_for_order_statement(order_option),
743 :limit => options[:limit],
744 :offset => options[:offset]).find_ids
570 rescue ::ActiveRecord::StatementInvalid => e 745 rescue ::ActiveRecord::StatementInvalid => e
571 raise StatementInvalid.new(e.message) 746 raise StatementInvalid.new(e.message)
572 end 747 end
573 748
574 # Returns the journals 749 # Returns the journals
625 sw = operator == "!*" ? 'NOT' : '' 800 sw = operator == "!*" ? 'NOT' : ''
626 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' 801 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
627 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" + 802 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
628 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))" 803 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
629 when "=", "!" 804 when "=", "!"
630 role_cond = value.any? ? 805 role_cond = value.any? ?
631 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" : 806 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
632 "1=0" 807 "1=0"
633 808
634 sw = operator == "!" ? 'NOT' : '' 809 sw = operator == "!" ? 'NOT' : ''
635 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' 810 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
636 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" + 811 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
637 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))" 812 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
638 end 813 end
639 end 814 end
640 815
816 def sql_for_is_private_field(field, operator, value)
817 op = (operator == "=" ? 'IN' : 'NOT IN')
818 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
819
820 "#{Issue.table_name}.is_private #{op} (#{va})"
821 end
822
823 def sql_for_relations(field, operator, value, options={})
824 relation_options = IssueRelation::TYPES[field]
825 return relation_options unless relation_options
826
827 relation_type = field
828 join_column, target_join_column = "issue_from_id", "issue_to_id"
829 if relation_options[:reverse] || options[:reverse]
830 relation_type = relation_options[:reverse] || relation_type
831 join_column, target_join_column = target_join_column, join_column
832 end
833
834 sql = case operator
835 when "*", "!*"
836 op = (operator == "*" ? 'IN' : 'NOT IN')
837 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
838 when "=", "!"
839 op = (operator == "=" ? 'IN' : 'NOT IN')
840 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
841 when "=p", "=!p", "!p"
842 op = (operator == "!p" ? 'NOT IN' : 'IN')
843 comp = (operator == "=!p" ? '<>' : '=')
844 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
845 end
846
847 if relation_options[:sym] == field && !options[:reverse]
848 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
849 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
850 else
851 sql
852 end
853 end
854
855 IssueRelation::TYPES.keys.each do |relation_type|
856 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
857 end
858
641 private 859 private
642 860
643 def sql_for_custom_field(field, operator, value, custom_field_id) 861 def sql_for_custom_field(field, operator, value, custom_field_id)
644 db_table = CustomValue.table_name 862 db_table = CustomValue.table_name
645 db_field = 'value' 863 db_field = 'value'
646 "#{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=#{custom_field_id} WHERE " + 864 filter = @available_filters[field]
865 return nil unless filter
866 if filter[:format] == 'user'
867 if value.delete('me')
868 value.push User.current.id.to_s
869 end
870 end
871 not_in = nil
872 if operator == '!'
873 # Makes ! operator work for custom fields with multiple values
874 operator = '='
875 not_in = 'NOT'
876 end
877 customized_key = "id"
878 customized_class = Issue
879 if field =~ /^(.+)\.cf_/
880 assoc = $1
881 customized_key = "#{assoc}_id"
882 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
883 raise "Unknown Issue association #{assoc}" unless customized_class
884 end
885 "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
647 sql_for_field(field, operator, value, db_table, db_field, true) + ')' 886 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
648 end 887 end
649 888
650 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ 889 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
651 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) 890 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
655 if value.any? 894 if value.any?
656 case type_for(field) 895 case type_for(field)
657 when :date, :date_past 896 when :date, :date_past
658 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil)) 897 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
659 when :integer 898 when :integer
660 sql = "#{db_table}.#{db_field} = #{value.first.to_i}" 899 if is_custom_filter
900 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
901 else
902 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
903 end
661 when :float 904 when :float
662 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}" 905 if is_custom_filter
906 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
907 else
908 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
909 end
663 else 910 else
664 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" 911 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
665 end 912 end
666 else 913 else
667 # IN an empty set 914 # IN an empty set
683 when ">=" 930 when ">="
684 if [:date, :date_past].include?(type_for(field)) 931 if [:date, :date_past].include?(type_for(field))
685 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil) 932 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
686 else 933 else
687 if is_custom_filter 934 if is_custom_filter
688 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f}" 935 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
689 else 936 else
690 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}" 937 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
691 end 938 end
692 end 939 end
693 when "<=" 940 when "<="
694 if [:date, :date_past].include?(type_for(field)) 941 if [:date, :date_past].include?(type_for(field))
695 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil)) 942 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
696 else 943 else
697 if is_custom_filter 944 if is_custom_filter
698 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f}" 945 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
699 else 946 else
700 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}" 947 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
701 end 948 end
702 end 949 end
703 when "><" 950 when "><"
704 if [:date, :date_past].include?(type_for(field)) 951 if [:date, :date_past].include?(type_for(field))
705 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil)) 952 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
706 else 953 else
707 if is_custom_filter 954 if is_custom_filter
708 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f}" 955 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
709 else 956 else
710 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}" 957 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
711 end 958 end
712 end 959 end
713 when "o" 960 when "o"
714 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" 961 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
715 when "c" 962 when "c"
716 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id" 963 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
964 when "><t-"
965 # between today - n days and today
966 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
717 when ">t-" 967 when ">t-"
718 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0) 968 # >= today - n days
969 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
719 when "<t-" 970 when "<t-"
971 # <= today - n days
720 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i) 972 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
721 when "t-" 973 when "t-"
974 # = n days in past
722 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i) 975 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
976 when "><t+"
977 # between today and today + n days
978 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
723 when ">t+" 979 when ">t+"
980 # >= today + n days
724 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil) 981 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
725 when "<t+" 982 when "<t+"
726 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i) 983 # <= today + n days
984 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
727 when "t+" 985 when "t+"
986 # = today + n days
728 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i) 987 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
729 when "t" 988 when "t"
989 # = today
730 sql = relative_date_clause(db_table, db_field, 0, 0) 990 sql = relative_date_clause(db_table, db_field, 0, 0)
731 when "w" 991 when "w"
992 # = this week
732 first_day_of_week = l(:general_first_day_of_week).to_i 993 first_day_of_week = l(:general_first_day_of_week).to_i
733 day_of_week = Date.today.cwday 994 day_of_week = Date.today.cwday
734 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) 995 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
735 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6) 996 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
736 when "~" 997 when "~"
742 end 1003 end
743 1004
744 return sql 1005 return sql
745 end 1006 end
746 1007
747 def add_custom_fields_filters(custom_fields) 1008 def add_custom_fields_filters(custom_fields, assoc=nil)
1009 return unless custom_fields.present?
748 @available_filters ||= {} 1010 @available_filters ||= {}
749 1011
750 custom_fields.select(&:is_filter?).each do |field| 1012 custom_fields.select(&:is_filter?).each do |field|
751 case field.field_format 1013 case field.field_format
752 when "text" 1014 when "text"
761 options = { :type => :integer, :order => 20 } 1023 options = { :type => :integer, :order => 20 }
762 when "float" 1024 when "float"
763 options = { :type => :float, :order => 20 } 1025 options = { :type => :float, :order => 20 }
764 when "user", "version" 1026 when "user", "version"
765 next unless project 1027 next unless project
766 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20} 1028 values = field.possible_values_options(project)
1029 if User.current.logged? && field.field_format == 'user'
1030 values.unshift ["<< #{l(:label_me)} >>", "me"]
1031 end
1032 options = { :type => :list_optional, :values => values, :order => 20}
767 else 1033 else
768 options = { :type => :string, :order => 20 } 1034 options = { :type => :string, :order => 20 }
769 end 1035 end
770 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) 1036 filter_id = "cf_#{field.id}"
1037 filter_name = field.name
1038 if assoc.present?
1039 filter_id = "#{assoc}.#{filter_id}"
1040 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1041 end
1042 @available_filters[filter_id] = options.merge({
1043 :name => filter_name,
1044 :format => field.field_format,
1045 :field => field
1046 })
1047 end
1048 end
1049
1050 def add_associations_custom_fields_filters(*associations)
1051 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1052 associations.each do |assoc|
1053 association_klass = Issue.reflect_on_association(assoc).klass
1054 fields_by_class.each do |field_class, fields|
1055 if field_class.customized_class <= association_klass
1056 add_custom_fields_filters(fields, assoc)
1057 end
1058 end
771 end 1059 end
772 end 1060 end
773 1061
774 # Returns a SQL clause for a date or datetime field. 1062 # Returns a SQL clause for a date or datetime field.
775 def date_clause(table, field, from, to) 1063 def date_clause(table, field, from, to)
776 s = [] 1064 s = []
777 if from 1065 if from
778 from_yesterday = from - 1 1066 from_yesterday = from - 1
779 from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day) 1067 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
780 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)]) 1068 if self.class.default_timezone == :utc
1069 from_yesterday_time = from_yesterday_time.utc
1070 end
1071 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
781 end 1072 end
782 if to 1073 if to
783 to_utc = Time.gm(to.year, to.month, to.day) 1074 to_time = Time.local(to.year, to.month, to.day)
784 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)]) 1075 if self.class.default_timezone == :utc
1076 to_time = to_time.utc
1077 end
1078 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
785 end 1079 end
786 s.join(' AND ') 1080 s.join(' AND ')
787 end 1081 end
788 1082
789 # Returns a SQL clause for a date or datetime field using relative dates. 1083 # Returns a SQL clause for a date or datetime field using relative dates.
790 def relative_date_clause(table, field, days_from, days_to) 1084 def relative_date_clause(table, field, days_from, days_to)
791 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil)) 1085 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
792 end 1086 end
1087
1088 # Additional joins required for the given sort options
1089 def joins_for_order_statement(order_options)
1090 joins = []
1091
1092 if order_options
1093 if order_options.include?('authors')
1094 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1095 end
1096 order_options.scan(/cf_\d+/).uniq.each do |name|
1097 column = available_columns.detect {|c| c.name.to_s == name}
1098 join = column && column.custom_field.join_for_order_statement
1099 if join
1100 joins << join
1101 end
1102 end
1103 end
1104
1105 joins.any? ? joins.join(' ') : nil
1106 end
793 end 1107 end