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 "!"
|