1
|
# Redmine - project management software
|
2
|
# Copyright (C) 2006-2016 Jean-Philippe Lang
|
3
|
#
|
4
|
# This program is free software; you can redistribute it and/or
|
5
|
# modify it under the terms of the GNU General Public License
|
6
|
# as published by the Free Software Foundation; either version 2
|
7
|
# of the License, or (at your option) any later version.
|
8
|
#
|
9
|
# This program is distributed in the hope that it will be useful,
|
10
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
# GNU General Public License for more details.
|
13
|
#
|
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
|
16
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
|
|
18
|
class IssueQuery < Query
|
19
|
|
20
|
self.queried_class = Issue
|
21
|
|
22
|
self.available_columns = [
|
23
|
QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
|
24
|
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
|
25
|
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
|
26
|
QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
|
27
|
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
|
28
|
QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
|
29
|
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
|
30
|
QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
|
31
|
QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
|
32
|
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
|
33
|
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
|
34
|
QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
|
35
|
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
|
36
|
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
|
37
|
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true),
|
38
|
QueryColumn.new(:total_estimated_hours,
|
39
|
:sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
|
40
|
" WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
|
41
|
:default_order => 'desc'),
|
42
|
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
|
43
|
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
|
44
|
QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
|
45
|
QueryColumn.new(:relations, :caption => :label_related_issues),
|
46
|
QueryColumn.new(:description, :inline => false)
|
47
|
]
|
48
|
|
49
|
scope :visible, lambda {|*args|
|
50
|
user = args.shift || User.current
|
51
|
base = Project.allowed_to_condition(user, :view_issues, *args)
|
52
|
scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
|
53
|
where("#{table_name}.project_id IS NULL OR (#{base})")
|
54
|
|
55
|
if user.admin?
|
56
|
scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
|
57
|
elsif user.memberships.any?
|
58
|
scope.where("#{table_name}.visibility = ?" +
|
59
|
" OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
|
60
|
"SELECT DISTINCT q.id FROM #{table_name} q" +
|
61
|
" INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
|
62
|
" INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
|
63
|
" INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
|
64
|
" WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
|
65
|
" OR #{table_name}.user_id = ?",
|
66
|
VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
|
67
|
elsif user.logged?
|
68
|
scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
|
69
|
else
|
70
|
scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
|
71
|
end
|
72
|
}
|
73
|
|
74
|
def initialize(attributes=nil, *args)
|
75
|
super attributes
|
76
|
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
|
77
|
end
|
78
|
|
79
|
# Returns true if the query is visible to +user+ or the current user.
|
80
|
def visible?(user=User.current)
|
81
|
return true if user.admin?
|
82
|
return false unless project.nil? || user.allowed_to?(:view_issues, project)
|
83
|
case visibility
|
84
|
when VISIBILITY_PUBLIC
|
85
|
true
|
86
|
when VISIBILITY_ROLES
|
87
|
if project
|
88
|
(user.roles_for_project(project) & roles).any?
|
89
|
else
|
90
|
Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
|
91
|
end
|
92
|
else
|
93
|
user == self.user
|
94
|
end
|
95
|
end
|
96
|
|
97
|
def is_private?
|
98
|
visibility == VISIBILITY_PRIVATE
|
99
|
end
|
100
|
|
101
|
def is_public?
|
102
|
!is_private?
|
103
|
end
|
104
|
|
105
|
def draw_relations
|
106
|
r = options[:draw_relations]
|
107
|
r.nil? || r == '1'
|
108
|
end
|
109
|
|
110
|
def draw_relations=(arg)
|
111
|
options[:draw_relations] = (arg == '0' ? '0' : nil)
|
112
|
end
|
113
|
|
114
|
def draw_progress_line
|
115
|
r = options[:draw_progress_line]
|
116
|
r == '1'
|
117
|
end
|
118
|
|
119
|
def draw_progress_line=(arg)
|
120
|
options[:draw_progress_line] = (arg == '1' ? '1' : nil)
|
121
|
end
|
122
|
|
123
|
def build_from_params(params)
|
124
|
super
|
125
|
self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
|
126
|
self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
|
127
|
self
|
128
|
end
|
129
|
|
130
|
def initialize_available_filters
|
131
|
principals = []
|
132
|
subprojects = []
|
133
|
versions = []
|
134
|
categories = []
|
135
|
issue_custom_fields = []
|
136
|
|
137
|
if project
|
138
|
principals += project.principals.visible
|
139
|
unless project.leaf?
|
140
|
subprojects = project.descendants.visible.to_a
|
141
|
principals += Principal.member_of(subprojects).visible
|
142
|
end
|
143
|
versions = project.shared_versions.to_a
|
144
|
categories = project.issue_categories.to_a
|
145
|
issue_custom_fields = project.all_issue_custom_fields
|
146
|
else
|
147
|
if all_projects.any?
|
148
|
principals += Principal.member_of(all_projects).visible
|
149
|
end
|
150
|
versions = Version.visible.where(:sharing => 'system').to_a
|
151
|
issue_custom_fields = IssueCustomField.where(:is_for_all => true)
|
152
|
end
|
153
|
principals.uniq!
|
154
|
principals.sort!
|
155
|
principals.reject! {|p| p.is_a?(GroupBuiltin)}
|
156
|
users = principals.select {|p| p.is_a?(User)}
|
157
|
|
158
|
add_available_filter "status_id",
|
159
|
:type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] }
|
160
|
|
161
|
if project.nil?
|
162
|
project_values = []
|
163
|
if User.current.logged? && User.current.memberships.any?
|
164
|
project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
|
165
|
end
|
166
|
project_values += all_projects_values
|
167
|
add_available_filter("project_id",
|
168
|
:type => :list, :values => project_values
|
169
|
) unless project_values.empty?
|
170
|
end
|
171
|
|
172
|
add_available_filter "tracker_id",
|
173
|
:type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
|
174
|
add_available_filter "priority_id",
|
175
|
:type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
|
176
|
|
177
|
author_values = []
|
178
|
author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
179
|
author_values += users.collect{|s| [s.name, s.id.to_s] }
|
180
|
add_available_filter("author_id",
|
181
|
:type => :list, :values => author_values
|
182
|
) unless author_values.empty?
|
183
|
|
184
|
assigned_to_values = []
|
185
|
assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
186
|
assigned_to_values += (Setting.issue_group_assignment? ?
|
187
|
principals : users).collect{|s| [s.name, s.id.to_s] }
|
188
|
add_available_filter("assigned_to_id",
|
189
|
:type => :list_optional, :values => assigned_to_values
|
190
|
) unless assigned_to_values.empty?
|
191
|
|
192
|
group_values = Group.givable.visible.collect {|g| [g.name, g.id.to_s] }
|
193
|
add_available_filter("member_of_group",
|
194
|
:type => :list_optional, :values => group_values
|
195
|
) unless group_values.empty?
|
196
|
|
197
|
role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
|
198
|
add_available_filter("assigned_to_role",
|
199
|
:type => :list_optional, :values => role_values
|
200
|
) unless role_values.empty?
|
201
|
|
202
|
add_available_filter "fixed_version_id",
|
203
|
:type => :list_optional,
|
204
|
:values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
|
205
|
|
206
|
add_available_filter "category_id",
|
207
|
:type => :list_optional,
|
208
|
:values => categories.collect{|s| [s.name, s.id.to_s] }
|
209
|
|
210
|
add_available_filter "subject", :type => :text
|
211
|
add_available_filter "description", :type => :text
|
212
|
add_available_filter "created_on", :type => :date_past
|
213
|
add_available_filter "updated_on", :type => :date_past
|
214
|
add_available_filter "closed_on", :type => :date_past
|
215
|
add_available_filter "start_date", :type => :date
|
216
|
add_available_filter "due_date", :type => :date
|
217
|
add_available_filter "estimated_hours", :type => :float
|
218
|
add_available_filter "done_ratio", :type => :integer
|
219
|
|
220
|
if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
|
221
|
User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
|
222
|
add_available_filter "is_private",
|
223
|
:type => :list,
|
224
|
:values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
|
225
|
end
|
226
|
|
227
|
if User.current.logged?
|
228
|
add_available_filter "watcher_id",
|
229
|
:type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
|
230
|
end
|
231
|
|
232
|
if subprojects.any?
|
233
|
add_available_filter "subproject_id",
|
234
|
:type => :list_subprojects,
|
235
|
:values => subprojects.collect{|s| [s.name, s.id.to_s] }
|
236
|
end
|
237
|
|
238
|
add_custom_fields_filters(issue_custom_fields)
|
239
|
|
240
|
add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
|
241
|
|
242
|
IssueRelation::TYPES.each do |relation_type, options|
|
243
|
add_available_filter relation_type, :type => :relation, :label => options[:name]
|
244
|
end
|
245
|
add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue
|
246
|
add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural
|
247
|
|
248
|
Tracker.disabled_core_fields(trackers).each {|field|
|
249
|
delete_available_filter field
|
250
|
}
|
251
|
end
|
252
|
|
253
|
def available_columns
|
254
|
return @available_columns if @available_columns
|
255
|
@available_columns = self.class.available_columns.dup
|
256
|
@available_columns += (project ?
|
257
|
project.all_issue_custom_fields :
|
258
|
IssueCustomField
|
259
|
).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
|
260
|
|
261
|
if User.current.allowed_to?(:view_time_entries, project, :global => true)
|
262
|
index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
|
263
|
index = (index ? index + 1 : -1)
|
264
|
# insert the column after total_estimated_hours or at the end
|
265
|
@available_columns.insert index, QueryColumn.new(:spent_hours,
|
266
|
:sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
|
267
|
:default_order => 'desc',
|
268
|
:caption => :label_spent_time,
|
269
|
:totalable => true
|
270
|
)
|
271
|
@available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
|
272
|
:sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" +
|
273
|
" WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
|
274
|
:default_order => 'desc',
|
275
|
:caption => :label_total_spent_time
|
276
|
)
|
277
|
end
|
278
|
|
279
|
if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
|
280
|
User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
|
281
|
@available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
|
282
|
end
|
283
|
|
284
|
disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
|
285
|
@available_columns.reject! {|column|
|
286
|
disabled_fields.include?(column.name.to_s)
|
287
|
}
|
288
|
|
289
|
@available_columns
|
290
|
end
|
291
|
|
292
|
def default_columns_names
|
293
|
@default_columns_names ||= begin
|
294
|
default_columns = Setting.issue_list_default_columns.map(&:to_sym)
|
295
|
|
296
|
project.present? ? default_columns : [:project] | default_columns
|
297
|
end
|
298
|
end
|
299
|
|
300
|
def base_scope
|
301
|
Issue.visible.joins(:status, :project).where(statement)
|
302
|
end
|
303
|
|
304
|
# Returns the issue count
|
305
|
def issue_count
|
306
|
base_scope.count
|
307
|
rescue ::ActiveRecord::StatementInvalid => e
|
308
|
raise StatementInvalid.new(e.message)
|
309
|
end
|
310
|
|
311
|
# Returns the issue count by group or nil if query is not grouped
|
312
|
def issue_count_by_group
|
313
|
grouped_query do |scope|
|
314
|
scope.count
|
315
|
end
|
316
|
end
|
317
|
|
318
|
# Returns sum of all the issue's estimated_hours
|
319
|
def total_for_estimated_hours(scope)
|
320
|
map_total(scope.sum(:estimated_hours)) {|t| t.to_f.round(2)}
|
321
|
end
|
322
|
|
323
|
# Returns sum of all the issue's time entries hours
|
324
|
def total_for_spent_hours(scope)
|
325
|
total = if group_by_column.try(:name) == :project
|
326
|
# TODO: remove this when https://github.com/rails/rails/issues/21922 is fixed
|
327
|
# We have to do a custom join without the time_entries.project_id column
|
328
|
# that would trigger a ambiguous column name error
|
329
|
scope.joins("JOIN (SELECT issue_id, hours FROM #{TimeEntry.table_name}) AS joined_time_entries ON joined_time_entries.issue_id = #{Issue.table_name}.id").
|
330
|
sum("joined_time_entries.hours")
|
331
|
else
|
332
|
scope.joins(:time_entries).sum("#{TimeEntry.table_name}.hours")
|
333
|
end
|
334
|
map_total(total) {|t| t.to_f.round(2)}
|
335
|
end
|
336
|
|
337
|
# Returns the issues
|
338
|
# Valid options are :order, :offset, :limit, :include, :conditions
|
339
|
def issues(options={})
|
340
|
if group_by_sort_order.is_a?(String)
|
341
|
# convert String in Array
|
342
|
order_option = [group_by_sort_order.split(','), options[:order]].flatten.reject(&:blank?)
|
343
|
else
|
344
|
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
|
345
|
end
|
346
|
|
347
|
scope = Issue.visible.
|
348
|
joins(:status, :project).
|
349
|
where(statement).
|
350
|
includes(([:status, :project] + (options[:include] || [])).uniq).
|
351
|
where(options[:conditions]).
|
352
|
order(order_option).
|
353
|
joins(joins_for_order_statement(order_option.join(','))).
|
354
|
limit(options[:limit]).
|
355
|
offset(options[:offset])
|
356
|
|
357
|
scope = scope.preload(:custom_values)
|
358
|
if has_column?(:author)
|
359
|
scope = scope.preload(:author)
|
360
|
end
|
361
|
|
362
|
issues = scope.to_a
|
363
|
|
364
|
if has_column?(:spent_hours)
|
365
|
Issue.load_visible_spent_hours(issues)
|
366
|
end
|
367
|
if has_column?(:total_spent_hours)
|
368
|
Issue.load_visible_total_spent_hours(issues)
|
369
|
end
|
370
|
if has_column?(:relations)
|
371
|
Issue.load_visible_relations(issues)
|
372
|
end
|
373
|
issues
|
374
|
rescue ::ActiveRecord::StatementInvalid => e
|
375
|
raise StatementInvalid.new(e.message)
|
376
|
end
|
377
|
|
378
|
# Returns the issues ids
|
379
|
def issue_ids(options={})
|
380
|
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
|
381
|
|
382
|
Issue.visible.
|
383
|
joins(:status, :project).
|
384
|
where(statement).
|
385
|
includes(([:status, :project] + (options[:include] || [])).uniq).
|
386
|
references(([:status, :project] + (options[:include] || [])).uniq).
|
387
|
where(options[:conditions]).
|
388
|
order(order_option).
|
389
|
joins(joins_for_order_statement(order_option.join(','))).
|
390
|
limit(options[:limit]).
|
391
|
offset(options[:offset]).
|
392
|
pluck(:id)
|
393
|
rescue ::ActiveRecord::StatementInvalid => e
|
394
|
raise StatementInvalid.new(e.message)
|
395
|
end
|
396
|
|
397
|
# Returns the journals
|
398
|
# Valid options are :order, :offset, :limit
|
399
|
def journals(options={})
|
400
|
Journal.visible.
|
401
|
joins(:issue => [:project, :status]).
|
402
|
where(statement).
|
403
|
order(options[:order]).
|
404
|
limit(options[:limit]).
|
405
|
offset(options[:offset]).
|
406
|
preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
|
407
|
to_a
|
408
|
rescue ::ActiveRecord::StatementInvalid => e
|
409
|
raise StatementInvalid.new(e.message)
|
410
|
end
|
411
|
|
412
|
# Returns the versions
|
413
|
# Valid options are :conditions
|
414
|
def versions(options={})
|
415
|
Version.visible.
|
416
|
where(project_statement).
|
417
|
where(options[:conditions]).
|
418
|
includes(:project).
|
419
|
references(:project).
|
420
|
to_a
|
421
|
rescue ::ActiveRecord::StatementInvalid => e
|
422
|
raise StatementInvalid.new(e.message)
|
423
|
end
|
424
|
|
425
|
def sql_for_watcher_id_field(field, operator, value)
|
426
|
db_table = Watcher.table_name
|
427
|
"#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
|
428
|
sql_for_field(field, '=', value, db_table, 'user_id') + ')'
|
429
|
end
|
430
|
|
431
|
def sql_for_member_of_group_field(field, operator, value)
|
432
|
if operator == '*' # Any group
|
433
|
groups = Group.givable
|
434
|
operator = '=' # Override the operator since we want to find by assigned_to
|
435
|
elsif operator == "!*"
|
436
|
groups = Group.givable
|
437
|
operator = '!' # Override the operator since we want to find by assigned_to
|
438
|
else
|
439
|
groups = Group.where(:id => value).to_a
|
440
|
end
|
441
|
groups ||= []
|
442
|
|
443
|
members_of_groups = groups.inject([]) {|user_ids, group|
|
444
|
user_ids + group.user_ids + [group.id]
|
445
|
}.uniq.compact.sort.collect(&:to_s)
|
446
|
|
447
|
'(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
|
448
|
end
|
449
|
|
450
|
def sql_for_assigned_to_role_field(field, operator, value)
|
451
|
case operator
|
452
|
when "*", "!*" # Member / Not member
|
453
|
sw = operator == "!*" ? 'NOT' : ''
|
454
|
nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
|
455
|
"(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
|
456
|
" WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
|
457
|
when "=", "!"
|
458
|
role_cond = value.any? ?
|
459
|
"#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")" :
|
460
|
"1=0"
|
461
|
|
462
|
sw = operator == "!" ? 'NOT' : ''
|
463
|
nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
|
464
|
"(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
|
465
|
" WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
|
466
|
end
|
467
|
end
|
468
|
|
469
|
def sql_for_is_private_field(field, operator, value)
|
470
|
op = (operator == "=" ? 'IN' : 'NOT IN')
|
471
|
va = value.map {|v| v == '0' ? self.class.connection.quoted_false : self.class.connection.quoted_true}.uniq.join(',')
|
472
|
|
473
|
"#{Issue.table_name}.is_private #{op} (#{va})"
|
474
|
end
|
475
|
|
476
|
def sql_for_parent_id_field(field, operator, value)
|
477
|
case operator
|
478
|
when "="
|
479
|
"#{Issue.table_name}.parent_id = #{value.first.to_i}"
|
480
|
when "~"
|
481
|
root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
|
482
|
if root_id && lft && rgt
|
483
|
"#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft > #{lft} AND #{Issue.table_name}.rgt < #{rgt}"
|
484
|
else
|
485
|
"1=0"
|
486
|
end
|
487
|
when "!*"
|
488
|
"#{Issue.table_name}.parent_id IS NULL"
|
489
|
when "*"
|
490
|
"#{Issue.table_name}.parent_id IS NOT NULL"
|
491
|
end
|
492
|
end
|
493
|
|
494
|
def sql_for_child_id_field(field, operator, value)
|
495
|
case operator
|
496
|
when "="
|
497
|
parent_id = Issue.where(:id => value.first.to_i).pluck(:parent_id).first
|
498
|
if parent_id
|
499
|
"#{Issue.table_name}.id = #{parent_id}"
|
500
|
else
|
501
|
"1=0"
|
502
|
end
|
503
|
when "~"
|
504
|
root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
|
505
|
if root_id && lft && rgt
|
506
|
"#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft < #{lft} AND #{Issue.table_name}.rgt > #{rgt}"
|
507
|
else
|
508
|
"1=0"
|
509
|
end
|
510
|
when "!*"
|
511
|
"#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1"
|
512
|
when "*"
|
513
|
"#{Issue.table_name}.rgt - #{Issue.table_name}.lft > 1"
|
514
|
end
|
515
|
end
|
516
|
|
517
|
def sql_for_relations(field, operator, value, options={})
|
518
|
relation_options = IssueRelation::TYPES[field]
|
519
|
return relation_options unless relation_options
|
520
|
|
521
|
relation_type = field
|
522
|
join_column, target_join_column = "issue_from_id", "issue_to_id"
|
523
|
if relation_options[:reverse] || options[:reverse]
|
524
|
relation_type = relation_options[:reverse] || relation_type
|
525
|
join_column, target_join_column = target_join_column, join_column
|
526
|
end
|
527
|
|
528
|
sql = case operator
|
529
|
when "*", "!*"
|
530
|
op = (operator == "*" ? 'IN' : 'NOT IN')
|
531
|
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}')"
|
532
|
when "=", "!"
|
533
|
op = (operator == "=" ? 'IN' : 'NOT IN')
|
534
|
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
|
535
|
when "=p", "=!p", "!p"
|
536
|
op = (operator == "!p" ? 'NOT IN' : 'IN')
|
537
|
comp = (operator == "=!p" ? '<>' : '=')
|
538
|
"#{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 = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
|
539
|
when "*o", "!o"
|
540
|
op = (operator == "!o" ? 'NOT IN' : 'IN')
|
541
|
"#{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 = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false}))"
|
542
|
end
|
543
|
|
544
|
if relation_options[:sym] == field && !options[:reverse]
|
545
|
sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
|
546
|
sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
|
547
|
end
|
548
|
"(#{sql})"
|
549
|
end
|
550
|
|
551
|
def find_assigned_to_id_filter_values(values)
|
552
|
Principal.visible.where(:id => values).map {|p| [p.name, p.id.to_s]}
|
553
|
end
|
554
|
alias :find_author_id_filter_values :find_assigned_to_id_filter_values
|
555
|
|
556
|
IssueRelation::TYPES.keys.each do |relation_type|
|
557
|
alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
|
558
|
end
|
559
|
end
|