| 16 |
16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
| 17 |
17 |
|
| 18 |
18 |
class QueryColumn
|
| 19 |
|
attr_accessor :name, :sortable, :groupable, :default_order
|
| 20 |
|
include Redmine::I18n
|
|
19 |
attr_accessor :name, :sortable, :default_order
|
|
20 |
include GLoc
|
| 21 |
21 |
|
| 22 |
22 |
def initialize(name, options={})
|
| 23 |
23 |
self.name = name
|
| 24 |
24 |
self.sortable = options[:sortable]
|
| 25 |
|
self.groupable = options[:groupable] || false
|
| 26 |
25 |
self.default_order = options[:default_order]
|
| 27 |
26 |
end
|
| 28 |
27 |
|
| 29 |
28 |
def caption
|
|
29 |
set_language_if_valid(User.current.language)
|
| 30 |
30 |
l("field_#{name}")
|
| 31 |
31 |
end
|
| 32 |
|
|
| 33 |
|
# Returns true if the column is sortable, otherwise false
|
| 34 |
|
def sortable?
|
| 35 |
|
!sortable.nil?
|
| 36 |
|
end
|
| 37 |
32 |
end
|
| 38 |
33 |
|
| 39 |
34 |
class QueryCustomFieldColumn < QueryColumn
|
| 40 |
35 |
|
| 41 |
36 |
def initialize(custom_field)
|
| 42 |
37 |
self.name = "cf_#{custom_field.id}".to_sym
|
| 43 |
|
self.sortable = custom_field.order_statement || false
|
|
38 |
self.sortable = false
|
| 44 |
39 |
@cf = custom_field
|
| 45 |
40 |
end
|
| 46 |
41 |
|
| ... | ... | |
| 58 |
53 |
belongs_to :user
|
| 59 |
54 |
serialize :filters
|
| 60 |
55 |
serialize :column_names
|
| 61 |
|
serialize :sort_criteria, Array
|
| 62 |
56 |
|
| 63 |
57 |
attr_protected :project_id, :user_id
|
| 64 |
58 |
|
| ... | ... | |
| 71 |
65 |
"c" => :label_closed_issues,
|
| 72 |
66 |
"!*" => :label_none,
|
| 73 |
67 |
"*" => :label_all,
|
| 74 |
|
">=" => :label_greater_or_equal,
|
| 75 |
|
"<=" => :label_less_or_equal,
|
|
68 |
">=" => '>=',
|
|
69 |
"<=" => '<=',
|
| 76 |
70 |
"<t+" => :label_in_less_than,
|
| 77 |
71 |
">t+" => :label_in_more_than,
|
| 78 |
72 |
"t+" => :label_in,
|
| ... | ... | |
| 99 |
93 |
cattr_reader :operators_by_filter_type
|
| 100 |
94 |
|
| 101 |
95 |
@@available_columns = [
|
| 102 |
|
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
|
| 103 |
|
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
|
| 104 |
|
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
|
| 105 |
|
QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc', :groupable => true),
|
|
96 |
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name"),
|
|
97 |
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
|
|
98 |
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
|
|
99 |
QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
|
| 106 |
100 |
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
|
| 107 |
101 |
QueryColumn.new(:author),
|
| 108 |
|
QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
|
|
102 |
QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
|
| 109 |
103 |
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
|
| 110 |
|
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
|
| 111 |
|
QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
|
|
104 |
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
|
|
105 |
QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
|
| 112 |
106 |
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
|
| 113 |
107 |
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
|
| 114 |
108 |
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
|
| 115 |
|
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
|
|
109 |
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
|
| 116 |
110 |
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
|
| 117 |
111 |
]
|
| 118 |
112 |
cattr_reader :available_columns
|
| ... | ... | |
| 120 |
114 |
def initialize(attributes = nil)
|
| 121 |
115 |
super attributes
|
| 122 |
116 |
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
|
|
117 |
set_language_if_valid(User.current.language)
|
| 123 |
118 |
end
|
| 124 |
119 |
|
| 125 |
120 |
def after_initialize
|
| ... | ... | |
| 129 |
124 |
|
| 130 |
125 |
def validate
|
| 131 |
126 |
filters.each_key do |field|
|
| 132 |
|
errors.add label_for(field), :blank unless
|
|
127 |
errors.add label_for(field), :activerecord_error_blank unless
|
| 133 |
128 |
# filter requires one or more values
|
| 134 |
129 |
(values_for(field) and !values_for(field).first.blank?) or
|
| 135 |
130 |
# filter doesn't require any value
|
| ... | ... | |
| 171 |
166 |
end
|
| 172 |
167 |
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
|
| 173 |
168 |
@available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
|
| 174 |
|
|
| 175 |
|
if User.current.logged?
|
| 176 |
|
@available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
|
| 177 |
|
end
|
| 178 |
169 |
|
| 179 |
170 |
if project
|
| 180 |
171 |
# project specific filters
|
| ... | ... | |
| 184 |
175 |
unless @project.versions.empty?
|
| 185 |
176 |
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
|
| 186 |
177 |
end
|
| 187 |
|
unless @project.descendants.active.empty?
|
| 188 |
|
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
|
|
178 |
unless @project.active_children.empty?
|
|
179 |
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
|
| 189 |
180 |
end
|
| 190 |
181 |
add_custom_fields_filters(@project.all_issue_custom_fields)
|
| 191 |
182 |
else
|
| ... | ... | |
| 238 |
229 |
@available_columns = Query.available_columns
|
| 239 |
230 |
@available_columns += (project ?
|
| 240 |
231 |
project.all_issue_custom_fields :
|
| 241 |
|
IssueCustomField.find(:all)
|
|
232 |
IssueCustomField.find(:all, :conditions => {:is_for_all => true})
|
| 242 |
233 |
).collect {|cf| QueryCustomFieldColumn.new(cf) }
|
| 243 |
234 |
end
|
| 244 |
235 |
|
| 245 |
|
# Returns an array of columns that can be used to group the results
|
| 246 |
|
def groupable_columns
|
| 247 |
|
available_columns.select {|c| c.groupable}
|
| 248 |
|
end
|
| 249 |
|
|
| 250 |
236 |
def columns
|
| 251 |
237 |
if has_default_columns?
|
| 252 |
238 |
available_columns.select do |c|
|
| ... | ... | |
| 273 |
259 |
column_names.nil? || column_names.empty?
|
| 274 |
260 |
end
|
| 275 |
261 |
|
| 276 |
|
def sort_criteria=(arg)
|
| 277 |
|
c = []
|
| 278 |
|
if arg.is_a?(Hash)
|
| 279 |
|
arg = arg.keys.sort.collect {|k| arg[k]}
|
| 280 |
|
end
|
| 281 |
|
c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
|
| 282 |
|
write_attribute(:sort_criteria, c)
|
| 283 |
|
end
|
| 284 |
|
|
| 285 |
|
def sort_criteria
|
| 286 |
|
read_attribute(:sort_criteria) || []
|
| 287 |
|
end
|
| 288 |
|
|
| 289 |
|
def sort_criteria_key(arg)
|
| 290 |
|
sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
|
| 291 |
|
end
|
| 292 |
|
|
| 293 |
|
def sort_criteria_order(arg)
|
| 294 |
|
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
|
| 295 |
|
end
|
| 296 |
|
|
| 297 |
|
# Returns the SQL sort order that should be prepended for grouping
|
| 298 |
|
def group_by_sort_order
|
| 299 |
|
if grouped? && (column = group_by_column)
|
| 300 |
|
column.sortable.is_a?(Array) ?
|
| 301 |
|
column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
|
| 302 |
|
"#{column.sortable} #{column.default_order}"
|
| 303 |
|
end
|
| 304 |
|
end
|
| 305 |
|
|
| 306 |
|
# Returns true if the query is a grouped query
|
| 307 |
|
def grouped?
|
| 308 |
|
!group_by.blank?
|
| 309 |
|
end
|
| 310 |
|
|
| 311 |
|
def group_by_column
|
| 312 |
|
groupable_columns.detect {|c| c.name.to_s == group_by}
|
| 313 |
|
end
|
| 314 |
|
|
| 315 |
262 |
def project_statement
|
| 316 |
263 |
project_clauses = []
|
| 317 |
|
if project && !@project.descendants.active.empty?
|
|
264 |
if project && !@project.active_children.empty?
|
| 318 |
265 |
ids = [project.id]
|
| 319 |
266 |
if has_filter?("subproject_id")
|
| 320 |
267 |
case operator_for("subproject_id")
|
| ... | ... | |
| 325 |
272 |
# main project only
|
| 326 |
273 |
else
|
| 327 |
274 |
# all subprojects
|
| 328 |
|
ids += project.descendants.collect(&:id)
|
|
275 |
ids += project.child_ids
|
| 329 |
276 |
end
|
| 330 |
277 |
elsif Setting.display_subprojects_issues?
|
| 331 |
|
ids += project.descendants.collect(&:id)
|
|
278 |
ids += project.child_ids
|
| 332 |
279 |
end
|
| 333 |
280 |
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
|
| 334 |
281 |
elsif project
|
| 335 |
282 |
project_clauses << "#{Project.table_name}.id = %d" % project.id
|
| 336 |
283 |
end
|
| 337 |
|
project_clauses << Project.allowed_to_condition(User.current, :view_issues)
|
| 338 |
284 |
project_clauses.join(' AND ')
|
| 339 |
285 |
end
|
| 340 |
286 |
|
| ... | ... | |
| 345 |
291 |
next if field == "subproject_id"
|
| 346 |
292 |
v = values_for(field).clone
|
| 347 |
293 |
next unless v and !v.empty?
|
| 348 |
|
operator = operator_for(field)
|
| 349 |
|
|
| 350 |
|
# "me" value subsitution
|
| 351 |
|
if %w(assigned_to_id author_id watcher_id).include?(field)
|
| 352 |
|
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
|
| 353 |
|
end
|
| 354 |
|
|
|
294 |
|
| 355 |
295 |
sql = ''
|
|
296 |
is_custom_filter = false
|
| 356 |
297 |
if field =~ /^cf_(\d+)$/
|
| 357 |
298 |
# custom field
|
| 358 |
299 |
db_table = CustomValue.table_name
|
| 359 |
300 |
db_field = 'value'
|
| 360 |
301 |
is_custom_filter = true
|
| 361 |
302 |
sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
|
| 362 |
|
sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
|
| 363 |
|
elsif field == 'watcher_id'
|
| 364 |
|
db_table = Watcher.table_name
|
| 365 |
|
db_field = 'user_id'
|
| 366 |
|
sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
|
| 367 |
|
sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
|
| 368 |
303 |
else
|
| 369 |
304 |
# regular field
|
| 370 |
305 |
db_table = Issue.table_name
|
| 371 |
306 |
db_field = field
|
| 372 |
|
sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
|
|
307 |
sql << '('
|
| 373 |
308 |
end
|
| 374 |
|
filters_clauses << sql
|
| 375 |
309 |
|
|
310 |
# "me" value subsitution
|
|
311 |
if %w(assigned_to_id author_id).include?(field)
|
|
312 |
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
|
|
313 |
end
|
|
314 |
|
|
315 |
sql = sql + sql_for_field(field, v, db_table, db_field, is_custom_filter)
|
|
316 |
|
|
317 |
sql << ')'
|
|
318 |
filters_clauses << sql
|
| 376 |
319 |
end if filters and valid?
|
| 377 |
320 |
|
|
321 |
permissions = '('
|
|
322 |
permissions << Project.allowed_to_condition(User.current, :view_issues)
|
|
323 |
permissions << ' OR ('
|
|
324 |
permissions << Project.allowed_to_condition(User.current, :view_own_issues)
|
|
325 |
permissions << ' AND '
|
|
326 |
permissions << "#{Issue.table_name}.author_id = #{User.current.id.to_s}"
|
|
327 |
permissions << '))'
|
|
328 |
|
|
329 |
filters_clauses << permissions
|
| 378 |
330 |
(filters_clauses << project_statement).join(' AND ')
|
| 379 |
331 |
end
|
| 380 |
332 |
|
| 381 |
333 |
private
|
| 382 |
334 |
|
| 383 |
|
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
|
| 384 |
|
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
|
|
335 |
# Helper method to generate the WHERE sql for a +field+ with a +value+
|
|
336 |
def sql_for_field(field, value, db_table, db_field, is_custom_filter)
|
| 385 |
337 |
sql = ''
|
| 386 |
|
case operator
|
|
338 |
case operator_for field
|
| 387 |
339 |
when "="
|
| 388 |
340 |
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
|
| 389 |
341 |
when "!"
|