##// END OF EJS Templates
Add users to assignee/author filters if they are missing (#3398)....
Jean-Philippe Lang -
r14340:828e8111cc77
parent child
Show More
@@ -1,550 +1,555
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssueQuery < Query
18 class IssueQuery < Query
19
19
20 self.queried_class = Issue
20 self.queried_class = Issue
21
21
22 self.available_columns = [
22 self.available_columns = [
23 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
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),
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :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),
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),
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),
28 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
29 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
29 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
30 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
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),
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'),
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),
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),
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"),
35 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
36 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_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),
37 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true),
38 QueryColumn.new(:total_estimated_hours,
38 QueryColumn.new(:total_estimated_hours,
39 :sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
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)",
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'),
41 :default_order => 'desc'),
42 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
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'),
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'),
44 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
45 QueryColumn.new(:relations, :caption => :label_related_issues),
45 QueryColumn.new(:relations, :caption => :label_related_issues),
46 QueryColumn.new(:description, :inline => false)
46 QueryColumn.new(:description, :inline => false)
47 ]
47 ]
48
48
49 scope :visible, lambda {|*args|
49 scope :visible, lambda {|*args|
50 user = args.shift || User.current
50 user = args.shift || User.current
51 base = Project.allowed_to_condition(user, :view_issues, *args)
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").
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})")
53 where("#{table_name}.project_id IS NULL OR (#{base})")
54
54
55 if user.admin?
55 if user.admin?
56 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
56 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
57 elsif user.memberships.any?
57 elsif user.memberships.any?
58 scope.where("#{table_name}.visibility = ?" +
58 scope.where("#{table_name}.visibility = ?" +
59 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
59 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
60 "SELECT DISTINCT q.id FROM #{table_name} q" +
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" +
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" +
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 = ?" +
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))" +
64 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
65 " OR #{table_name}.user_id = ?",
65 " OR #{table_name}.user_id = ?",
66 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
66 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
67 elsif user.logged?
67 elsif user.logged?
68 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
68 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
69 else
69 else
70 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
70 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
71 end
71 end
72 }
72 }
73
73
74 def initialize(attributes=nil, *args)
74 def initialize(attributes=nil, *args)
75 super attributes
75 super attributes
76 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
76 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
77 end
77 end
78
78
79 # Returns true if the query is visible to +user+ or the current user.
79 # Returns true if the query is visible to +user+ or the current user.
80 def visible?(user=User.current)
80 def visible?(user=User.current)
81 return true if user.admin?
81 return true if user.admin?
82 return false unless project.nil? || user.allowed_to?(:view_issues, project)
82 return false unless project.nil? || user.allowed_to?(:view_issues, project)
83 case visibility
83 case visibility
84 when VISIBILITY_PUBLIC
84 when VISIBILITY_PUBLIC
85 true
85 true
86 when VISIBILITY_ROLES
86 when VISIBILITY_ROLES
87 if project
87 if project
88 (user.roles_for_project(project) & roles).any?
88 (user.roles_for_project(project) & roles).any?
89 else
89 else
90 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
90 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
91 end
91 end
92 else
92 else
93 user == self.user
93 user == self.user
94 end
94 end
95 end
95 end
96
96
97 def is_private?
97 def is_private?
98 visibility == VISIBILITY_PRIVATE
98 visibility == VISIBILITY_PRIVATE
99 end
99 end
100
100
101 def is_public?
101 def is_public?
102 !is_private?
102 !is_private?
103 end
103 end
104
104
105 def draw_relations
105 def draw_relations
106 r = options[:draw_relations]
106 r = options[:draw_relations]
107 r.nil? || r == '1'
107 r.nil? || r == '1'
108 end
108 end
109
109
110 def draw_relations=(arg)
110 def draw_relations=(arg)
111 options[:draw_relations] = (arg == '0' ? '0' : nil)
111 options[:draw_relations] = (arg == '0' ? '0' : nil)
112 end
112 end
113
113
114 def draw_progress_line
114 def draw_progress_line
115 r = options[:draw_progress_line]
115 r = options[:draw_progress_line]
116 r == '1'
116 r == '1'
117 end
117 end
118
118
119 def draw_progress_line=(arg)
119 def draw_progress_line=(arg)
120 options[:draw_progress_line] = (arg == '1' ? '1' : nil)
120 options[:draw_progress_line] = (arg == '1' ? '1' : nil)
121 end
121 end
122
122
123 def build_from_params(params)
123 def build_from_params(params)
124 super
124 super
125 self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
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])
126 self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
127 self
127 self
128 end
128 end
129
129
130 def initialize_available_filters
130 def initialize_available_filters
131 principals = []
131 principals = []
132 subprojects = []
132 subprojects = []
133 versions = []
133 versions = []
134 categories = []
134 categories = []
135 issue_custom_fields = []
135 issue_custom_fields = []
136
136
137 if project
137 if project
138 principals += project.principals.visible
138 principals += project.principals.visible
139 unless project.leaf?
139 unless project.leaf?
140 subprojects = project.descendants.visible.to_a
140 subprojects = project.descendants.visible.to_a
141 principals += Principal.member_of(subprojects).visible
141 principals += Principal.member_of(subprojects).visible
142 end
142 end
143 versions = project.shared_versions.to_a
143 versions = project.shared_versions.to_a
144 categories = project.issue_categories.to_a
144 categories = project.issue_categories.to_a
145 issue_custom_fields = project.all_issue_custom_fields
145 issue_custom_fields = project.all_issue_custom_fields
146 else
146 else
147 if all_projects.any?
147 if all_projects.any?
148 principals += Principal.member_of(all_projects).visible
148 principals += Principal.member_of(all_projects).visible
149 end
149 end
150 versions = Version.visible.where(:sharing => 'system').to_a
150 versions = Version.visible.where(:sharing => 'system').to_a
151 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
151 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
152 end
152 end
153 principals.uniq!
153 principals.uniq!
154 principals.sort!
154 principals.sort!
155 principals.reject! {|p| p.is_a?(GroupBuiltin)}
155 principals.reject! {|p| p.is_a?(GroupBuiltin)}
156 users = principals.select {|p| p.is_a?(User)}
156 users = principals.select {|p| p.is_a?(User)}
157
157
158 add_available_filter "status_id",
158 add_available_filter "status_id",
159 :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] }
159 :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] }
160
160
161 if project.nil?
161 if project.nil?
162 project_values = []
162 project_values = []
163 if User.current.logged? && User.current.memberships.any?
163 if User.current.logged? && User.current.memberships.any?
164 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
164 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
165 end
165 end
166 project_values += all_projects_values
166 project_values += all_projects_values
167 add_available_filter("project_id",
167 add_available_filter("project_id",
168 :type => :list, :values => project_values
168 :type => :list, :values => project_values
169 ) unless project_values.empty?
169 ) unless project_values.empty?
170 end
170 end
171
171
172 add_available_filter "tracker_id",
172 add_available_filter "tracker_id",
173 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
173 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
174 add_available_filter "priority_id",
174 add_available_filter "priority_id",
175 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
175 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
176
176
177 author_values = []
177 author_values = []
178 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
178 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
179 author_values += users.collect{|s| [s.name, s.id.to_s] }
179 author_values += users.collect{|s| [s.name, s.id.to_s] }
180 add_available_filter("author_id",
180 add_available_filter("author_id",
181 :type => :list, :values => author_values
181 :type => :list, :values => author_values
182 ) unless author_values.empty?
182 ) unless author_values.empty?
183
183
184 assigned_to_values = []
184 assigned_to_values = []
185 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
185 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
186 assigned_to_values += (Setting.issue_group_assignment? ?
186 assigned_to_values += (Setting.issue_group_assignment? ?
187 principals : users).collect{|s| [s.name, s.id.to_s] }
187 principals : users).collect{|s| [s.name, s.id.to_s] }
188 add_available_filter("assigned_to_id",
188 add_available_filter("assigned_to_id",
189 :type => :list_optional, :values => assigned_to_values
189 :type => :list_optional, :values => assigned_to_values
190 ) unless assigned_to_values.empty?
190 ) unless assigned_to_values.empty?
191
191
192 group_values = Group.givable.visible.collect {|g| [g.name, g.id.to_s] }
192 group_values = Group.givable.visible.collect {|g| [g.name, g.id.to_s] }
193 add_available_filter("member_of_group",
193 add_available_filter("member_of_group",
194 :type => :list_optional, :values => group_values
194 :type => :list_optional, :values => group_values
195 ) unless group_values.empty?
195 ) unless group_values.empty?
196
196
197 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
197 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
198 add_available_filter("assigned_to_role",
198 add_available_filter("assigned_to_role",
199 :type => :list_optional, :values => role_values
199 :type => :list_optional, :values => role_values
200 ) unless role_values.empty?
200 ) unless role_values.empty?
201
201
202 if versions.any?
202 if versions.any?
203 add_available_filter "fixed_version_id",
203 add_available_filter "fixed_version_id",
204 :type => :list_optional,
204 :type => :list_optional,
205 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
205 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
206 end
206 end
207
207
208 if categories.any?
208 if categories.any?
209 add_available_filter "category_id",
209 add_available_filter "category_id",
210 :type => :list_optional,
210 :type => :list_optional,
211 :values => categories.collect{|s| [s.name, s.id.to_s] }
211 :values => categories.collect{|s| [s.name, s.id.to_s] }
212 end
212 end
213
213
214 add_available_filter "subject", :type => :text
214 add_available_filter "subject", :type => :text
215 add_available_filter "description", :type => :text
215 add_available_filter "description", :type => :text
216 add_available_filter "created_on", :type => :date_past
216 add_available_filter "created_on", :type => :date_past
217 add_available_filter "updated_on", :type => :date_past
217 add_available_filter "updated_on", :type => :date_past
218 add_available_filter "closed_on", :type => :date_past
218 add_available_filter "closed_on", :type => :date_past
219 add_available_filter "start_date", :type => :date
219 add_available_filter "start_date", :type => :date
220 add_available_filter "due_date", :type => :date
220 add_available_filter "due_date", :type => :date
221 add_available_filter "estimated_hours", :type => :float
221 add_available_filter "estimated_hours", :type => :float
222 add_available_filter "done_ratio", :type => :integer
222 add_available_filter "done_ratio", :type => :integer
223
223
224 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
224 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
225 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
225 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
226 add_available_filter "is_private",
226 add_available_filter "is_private",
227 :type => :list,
227 :type => :list,
228 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
228 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
229 end
229 end
230
230
231 if User.current.logged?
231 if User.current.logged?
232 add_available_filter "watcher_id",
232 add_available_filter "watcher_id",
233 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
233 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
234 end
234 end
235
235
236 if subprojects.any?
236 if subprojects.any?
237 add_available_filter "subproject_id",
237 add_available_filter "subproject_id",
238 :type => :list_subprojects,
238 :type => :list_subprojects,
239 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
239 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
240 end
240 end
241
241
242 add_custom_fields_filters(issue_custom_fields)
242 add_custom_fields_filters(issue_custom_fields)
243
243
244 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
244 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
245
245
246 IssueRelation::TYPES.each do |relation_type, options|
246 IssueRelation::TYPES.each do |relation_type, options|
247 add_available_filter relation_type, :type => :relation, :label => options[:name]
247 add_available_filter relation_type, :type => :relation, :label => options[:name]
248 end
248 end
249 add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue
249 add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue
250 add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural
250 add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural
251
251
252 Tracker.disabled_core_fields(trackers).each {|field|
252 Tracker.disabled_core_fields(trackers).each {|field|
253 delete_available_filter field
253 delete_available_filter field
254 }
254 }
255 end
255 end
256
256
257 def available_columns
257 def available_columns
258 return @available_columns if @available_columns
258 return @available_columns if @available_columns
259 @available_columns = self.class.available_columns.dup
259 @available_columns = self.class.available_columns.dup
260 @available_columns += (project ?
260 @available_columns += (project ?
261 project.all_issue_custom_fields :
261 project.all_issue_custom_fields :
262 IssueCustomField
262 IssueCustomField
263 ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
263 ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
264
264
265 if User.current.allowed_to?(:view_time_entries, project, :global => true)
265 if User.current.allowed_to?(:view_time_entries, project, :global => true)
266 index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
266 index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
267 index = (index ? index + 1 : -1)
267 index = (index ? index + 1 : -1)
268 # insert the column after total_estimated_hours or at the end
268 # insert the column after total_estimated_hours or at the end
269 @available_columns.insert index, QueryColumn.new(:spent_hours,
269 @available_columns.insert index, QueryColumn.new(:spent_hours,
270 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
270 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
271 :default_order => 'desc',
271 :default_order => 'desc',
272 :caption => :label_spent_time,
272 :caption => :label_spent_time,
273 :totalable => true
273 :totalable => true
274 )
274 )
275 @available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
275 @available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
276 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" +
276 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" +
277 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
277 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
278 :default_order => 'desc',
278 :default_order => 'desc',
279 :caption => :label_total_spent_time
279 :caption => :label_total_spent_time
280 )
280 )
281 end
281 end
282
282
283 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
283 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
284 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
284 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
285 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
285 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
286 end
286 end
287
287
288 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
288 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
289 @available_columns.reject! {|column|
289 @available_columns.reject! {|column|
290 disabled_fields.include?(column.name.to_s)
290 disabled_fields.include?(column.name.to_s)
291 }
291 }
292
292
293 @available_columns
293 @available_columns
294 end
294 end
295
295
296 def default_columns_names
296 def default_columns_names
297 @default_columns_names ||= begin
297 @default_columns_names ||= begin
298 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
298 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
299
299
300 project.present? ? default_columns : [:project] | default_columns
300 project.present? ? default_columns : [:project] | default_columns
301 end
301 end
302 end
302 end
303
303
304 def base_scope
304 def base_scope
305 Issue.visible.joins(:status, :project).where(statement)
305 Issue.visible.joins(:status, :project).where(statement)
306 end
306 end
307
307
308 # Returns the issue count
308 # Returns the issue count
309 def issue_count
309 def issue_count
310 base_scope.count
310 base_scope.count
311 rescue ::ActiveRecord::StatementInvalid => e
311 rescue ::ActiveRecord::StatementInvalid => e
312 raise StatementInvalid.new(e.message)
312 raise StatementInvalid.new(e.message)
313 end
313 end
314
314
315 # Returns the issue count by group or nil if query is not grouped
315 # Returns the issue count by group or nil if query is not grouped
316 def issue_count_by_group
316 def issue_count_by_group
317 grouped_query do |scope|
317 grouped_query do |scope|
318 scope.count
318 scope.count
319 end
319 end
320 end
320 end
321
321
322 # Returns sum of all the issue's estimated_hours
322 # Returns sum of all the issue's estimated_hours
323 def total_for_estimated_hours(scope)
323 def total_for_estimated_hours(scope)
324 map_total(scope.sum(:estimated_hours)) {|t| t.to_f.round(2)}
324 map_total(scope.sum(:estimated_hours)) {|t| t.to_f.round(2)}
325 end
325 end
326
326
327 # Returns sum of all the issue's time entries hours
327 # Returns sum of all the issue's time entries hours
328 def total_for_spent_hours(scope)
328 def total_for_spent_hours(scope)
329 total = if group_by_column.try(:name) == :project
329 total = if group_by_column.try(:name) == :project
330 # TODO: remove this when https://github.com/rails/rails/issues/21922 is fixed
330 # TODO: remove this when https://github.com/rails/rails/issues/21922 is fixed
331 # We have to do a custom join without the time_entries.project_id column
331 # We have to do a custom join without the time_entries.project_id column
332 # that would trigger a ambiguous column name error
332 # that would trigger a ambiguous column name error
333 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").
333 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").
334 sum("joined_time_entries.hours")
334 sum("joined_time_entries.hours")
335 else
335 else
336 scope.joins(:time_entries).sum("#{TimeEntry.table_name}.hours")
336 scope.joins(:time_entries).sum("#{TimeEntry.table_name}.hours")
337 end
337 end
338 map_total(total) {|t| t.to_f.round(2)}
338 map_total(total) {|t| t.to_f.round(2)}
339 end
339 end
340
340
341 # Returns the issues
341 # Returns the issues
342 # Valid options are :order, :offset, :limit, :include, :conditions
342 # Valid options are :order, :offset, :limit, :include, :conditions
343 def issues(options={})
343 def issues(options={})
344 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
344 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
345
345
346 scope = Issue.visible.
346 scope = Issue.visible.
347 joins(:status, :project).
347 joins(:status, :project).
348 where(statement).
348 where(statement).
349 includes(([:status, :project] + (options[:include] || [])).uniq).
349 includes(([:status, :project] + (options[:include] || [])).uniq).
350 where(options[:conditions]).
350 where(options[:conditions]).
351 order(order_option).
351 order(order_option).
352 joins(joins_for_order_statement(order_option.join(','))).
352 joins(joins_for_order_statement(order_option.join(','))).
353 limit(options[:limit]).
353 limit(options[:limit]).
354 offset(options[:offset])
354 offset(options[:offset])
355
355
356 scope = scope.preload(:custom_values)
356 scope = scope.preload(:custom_values)
357 if has_column?(:author)
357 if has_column?(:author)
358 scope = scope.preload(:author)
358 scope = scope.preload(:author)
359 end
359 end
360
360
361 issues = scope.to_a
361 issues = scope.to_a
362
362
363 if has_column?(:spent_hours)
363 if has_column?(:spent_hours)
364 Issue.load_visible_spent_hours(issues)
364 Issue.load_visible_spent_hours(issues)
365 end
365 end
366 if has_column?(:total_spent_hours)
366 if has_column?(:total_spent_hours)
367 Issue.load_visible_total_spent_hours(issues)
367 Issue.load_visible_total_spent_hours(issues)
368 end
368 end
369 if has_column?(:relations)
369 if has_column?(:relations)
370 Issue.load_visible_relations(issues)
370 Issue.load_visible_relations(issues)
371 end
371 end
372 issues
372 issues
373 rescue ::ActiveRecord::StatementInvalid => e
373 rescue ::ActiveRecord::StatementInvalid => e
374 raise StatementInvalid.new(e.message)
374 raise StatementInvalid.new(e.message)
375 end
375 end
376
376
377 # Returns the issues ids
377 # Returns the issues ids
378 def issue_ids(options={})
378 def issue_ids(options={})
379 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
379 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
380
380
381 Issue.visible.
381 Issue.visible.
382 joins(:status, :project).
382 joins(:status, :project).
383 where(statement).
383 where(statement).
384 includes(([:status, :project] + (options[:include] || [])).uniq).
384 includes(([:status, :project] + (options[:include] || [])).uniq).
385 references(([:status, :project] + (options[:include] || [])).uniq).
385 references(([:status, :project] + (options[:include] || [])).uniq).
386 where(options[:conditions]).
386 where(options[:conditions]).
387 order(order_option).
387 order(order_option).
388 joins(joins_for_order_statement(order_option.join(','))).
388 joins(joins_for_order_statement(order_option.join(','))).
389 limit(options[:limit]).
389 limit(options[:limit]).
390 offset(options[:offset]).
390 offset(options[:offset]).
391 pluck(:id)
391 pluck(:id)
392 rescue ::ActiveRecord::StatementInvalid => e
392 rescue ::ActiveRecord::StatementInvalid => e
393 raise StatementInvalid.new(e.message)
393 raise StatementInvalid.new(e.message)
394 end
394 end
395
395
396 # Returns the journals
396 # Returns the journals
397 # Valid options are :order, :offset, :limit
397 # Valid options are :order, :offset, :limit
398 def journals(options={})
398 def journals(options={})
399 Journal.visible.
399 Journal.visible.
400 joins(:issue => [:project, :status]).
400 joins(:issue => [:project, :status]).
401 where(statement).
401 where(statement).
402 order(options[:order]).
402 order(options[:order]).
403 limit(options[:limit]).
403 limit(options[:limit]).
404 offset(options[:offset]).
404 offset(options[:offset]).
405 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
405 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
406 to_a
406 to_a
407 rescue ::ActiveRecord::StatementInvalid => e
407 rescue ::ActiveRecord::StatementInvalid => e
408 raise StatementInvalid.new(e.message)
408 raise StatementInvalid.new(e.message)
409 end
409 end
410
410
411 # Returns the versions
411 # Returns the versions
412 # Valid options are :conditions
412 # Valid options are :conditions
413 def versions(options={})
413 def versions(options={})
414 Version.visible.
414 Version.visible.
415 where(project_statement).
415 where(project_statement).
416 where(options[:conditions]).
416 where(options[:conditions]).
417 includes(:project).
417 includes(:project).
418 references(:project).
418 references(:project).
419 to_a
419 to_a
420 rescue ::ActiveRecord::StatementInvalid => e
420 rescue ::ActiveRecord::StatementInvalid => e
421 raise StatementInvalid.new(e.message)
421 raise StatementInvalid.new(e.message)
422 end
422 end
423
423
424 def sql_for_watcher_id_field(field, operator, value)
424 def sql_for_watcher_id_field(field, operator, value)
425 db_table = Watcher.table_name
425 db_table = Watcher.table_name
426 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
426 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
427 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
427 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
428 end
428 end
429
429
430 def sql_for_member_of_group_field(field, operator, value)
430 def sql_for_member_of_group_field(field, operator, value)
431 if operator == '*' # Any group
431 if operator == '*' # Any group
432 groups = Group.givable
432 groups = Group.givable
433 operator = '=' # Override the operator since we want to find by assigned_to
433 operator = '=' # Override the operator since we want to find by assigned_to
434 elsif operator == "!*"
434 elsif operator == "!*"
435 groups = Group.givable
435 groups = Group.givable
436 operator = '!' # Override the operator since we want to find by assigned_to
436 operator = '!' # Override the operator since we want to find by assigned_to
437 else
437 else
438 groups = Group.where(:id => value).to_a
438 groups = Group.where(:id => value).to_a
439 end
439 end
440 groups ||= []
440 groups ||= []
441
441
442 members_of_groups = groups.inject([]) {|user_ids, group|
442 members_of_groups = groups.inject([]) {|user_ids, group|
443 user_ids + group.user_ids + [group.id]
443 user_ids + group.user_ids + [group.id]
444 }.uniq.compact.sort.collect(&:to_s)
444 }.uniq.compact.sort.collect(&:to_s)
445
445
446 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
446 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
447 end
447 end
448
448
449 def sql_for_assigned_to_role_field(field, operator, value)
449 def sql_for_assigned_to_role_field(field, operator, value)
450 case operator
450 case operator
451 when "*", "!*" # Member / Not member
451 when "*", "!*" # Member / Not member
452 sw = operator == "!*" ? 'NOT' : ''
452 sw = operator == "!*" ? 'NOT' : ''
453 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
453 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
454 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
454 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
455 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
455 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
456 when "=", "!"
456 when "=", "!"
457 role_cond = value.any? ?
457 role_cond = value.any? ?
458 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")" :
458 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")" :
459 "1=0"
459 "1=0"
460
460
461 sw = operator == "!" ? 'NOT' : ''
461 sw = operator == "!" ? 'NOT' : ''
462 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
462 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
463 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
463 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
464 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
464 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
465 end
465 end
466 end
466 end
467
467
468 def sql_for_is_private_field(field, operator, value)
468 def sql_for_is_private_field(field, operator, value)
469 op = (operator == "=" ? 'IN' : 'NOT IN')
469 op = (operator == "=" ? 'IN' : 'NOT IN')
470 va = value.map {|v| v == '0' ? self.class.connection.quoted_false : self.class.connection.quoted_true}.uniq.join(',')
470 va = value.map {|v| v == '0' ? self.class.connection.quoted_false : self.class.connection.quoted_true}.uniq.join(',')
471
471
472 "#{Issue.table_name}.is_private #{op} (#{va})"
472 "#{Issue.table_name}.is_private #{op} (#{va})"
473 end
473 end
474
474
475 def sql_for_parent_id_field(field, operator, value)
475 def sql_for_parent_id_field(field, operator, value)
476 case operator
476 case operator
477 when "="
477 when "="
478 "#{Issue.table_name}.parent_id = #{value.first.to_i}"
478 "#{Issue.table_name}.parent_id = #{value.first.to_i}"
479 when "~"
479 when "~"
480 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
480 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
481 if root_id && lft && rgt
481 if root_id && lft && rgt
482 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft > #{lft} AND #{Issue.table_name}.rgt < #{rgt}"
482 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft > #{lft} AND #{Issue.table_name}.rgt < #{rgt}"
483 else
483 else
484 "1=0"
484 "1=0"
485 end
485 end
486 when "!*"
486 when "!*"
487 "#{Issue.table_name}.parent_id IS NULL"
487 "#{Issue.table_name}.parent_id IS NULL"
488 when "*"
488 when "*"
489 "#{Issue.table_name}.parent_id IS NOT NULL"
489 "#{Issue.table_name}.parent_id IS NOT NULL"
490 end
490 end
491 end
491 end
492
492
493 def sql_for_child_id_field(field, operator, value)
493 def sql_for_child_id_field(field, operator, value)
494 case operator
494 case operator
495 when "="
495 when "="
496 parent_id = Issue.where(:id => value.first.to_i).pluck(:parent_id).first
496 parent_id = Issue.where(:id => value.first.to_i).pluck(:parent_id).first
497 if parent_id
497 if parent_id
498 "#{Issue.table_name}.id = #{parent_id}"
498 "#{Issue.table_name}.id = #{parent_id}"
499 else
499 else
500 "1=0"
500 "1=0"
501 end
501 end
502 when "~"
502 when "~"
503 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
503 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
504 if root_id && lft && rgt
504 if root_id && lft && rgt
505 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft < #{lft} AND #{Issue.table_name}.rgt > #{rgt}"
505 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft < #{lft} AND #{Issue.table_name}.rgt > #{rgt}"
506 else
506 else
507 "1=0"
507 "1=0"
508 end
508 end
509 when "!*"
509 when "!*"
510 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1"
510 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1"
511 when "*"
511 when "*"
512 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft > 1"
512 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft > 1"
513 end
513 end
514 end
514 end
515
515
516 def sql_for_relations(field, operator, value, options={})
516 def sql_for_relations(field, operator, value, options={})
517 relation_options = IssueRelation::TYPES[field]
517 relation_options = IssueRelation::TYPES[field]
518 return relation_options unless relation_options
518 return relation_options unless relation_options
519
519
520 relation_type = field
520 relation_type = field
521 join_column, target_join_column = "issue_from_id", "issue_to_id"
521 join_column, target_join_column = "issue_from_id", "issue_to_id"
522 if relation_options[:reverse] || options[:reverse]
522 if relation_options[:reverse] || options[:reverse]
523 relation_type = relation_options[:reverse] || relation_type
523 relation_type = relation_options[:reverse] || relation_type
524 join_column, target_join_column = target_join_column, join_column
524 join_column, target_join_column = target_join_column, join_column
525 end
525 end
526
526
527 sql = case operator
527 sql = case operator
528 when "*", "!*"
528 when "*", "!*"
529 op = (operator == "*" ? 'IN' : 'NOT IN')
529 op = (operator == "*" ? 'IN' : 'NOT IN')
530 "#{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)}')"
530 "#{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)}')"
531 when "=", "!"
531 when "=", "!"
532 op = (operator == "=" ? 'IN' : 'NOT IN')
532 op = (operator == "=" ? 'IN' : 'NOT IN')
533 "#{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})"
533 "#{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})"
534 when "=p", "=!p", "!p"
534 when "=p", "=!p", "!p"
535 op = (operator == "!p" ? 'NOT IN' : 'IN')
535 op = (operator == "!p" ? 'NOT IN' : 'IN')
536 comp = (operator == "=!p" ? '<>' : '=')
536 comp = (operator == "=!p" ? '<>' : '=')
537 "#{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})"
537 "#{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})"
538 end
538 end
539
539
540 if relation_options[:sym] == field && !options[:reverse]
540 if relation_options[:sym] == field && !options[:reverse]
541 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
541 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
542 sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
542 sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
543 end
543 end
544 "(#{sql})"
544 "(#{sql})"
545 end
545 end
546
546
547 def find_assigned_to_id_filter_values(values)
548 Principal.visible.where(:id => values).map {|p| [p.name, p.id.to_s]}
549 end
550 alias :find_author_id_filter_values :find_assigned_to_id_filter_values
551
547 IssueRelation::TYPES.keys.each do |relation_type|
552 IssueRelation::TYPES.keys.each do |relation_type|
548 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
553 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
549 end
554 end
550 end
555 end
@@ -1,1029 +1,1036
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :totalable, :default_order
19 attr_accessor :name, :sortable, :groupable, :totalable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.totalable = options[:totalable] || false
29 self.totalable = options[:totalable] || false
30 self.default_order = options[:default_order]
30 self.default_order = options[:default_order]
31 @inline = options.key?(:inline) ? options[:inline] : true
31 @inline = options.key?(:inline) ? options[:inline] : true
32 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @caption_key = options[:caption] || "field_#{name}".to_sym
33 @frozen = options[:frozen]
33 @frozen = options[:frozen]
34 end
34 end
35
35
36 def caption
36 def caption
37 case @caption_key
37 case @caption_key
38 when Symbol
38 when Symbol
39 l(@caption_key)
39 l(@caption_key)
40 when Proc
40 when Proc
41 @caption_key.call
41 @caption_key.call
42 else
42 else
43 @caption_key
43 @caption_key
44 end
44 end
45 end
45 end
46
46
47 # Returns true if the column is sortable, otherwise false
47 # Returns true if the column is sortable, otherwise false
48 def sortable?
48 def sortable?
49 !@sortable.nil?
49 !@sortable.nil?
50 end
50 end
51
51
52 def sortable
52 def sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
54 end
54 end
55
55
56 def inline?
56 def inline?
57 @inline
57 @inline
58 end
58 end
59
59
60 def frozen?
60 def frozen?
61 @frozen
61 @frozen
62 end
62 end
63
63
64 def value(object)
64 def value(object)
65 object.send name
65 object.send name
66 end
66 end
67
67
68 def value_object(object)
68 def value_object(object)
69 object.send name
69 object.send name
70 end
70 end
71
71
72 def css_classes
72 def css_classes
73 name
73 name
74 end
74 end
75 end
75 end
76
76
77 class QueryCustomFieldColumn < QueryColumn
77 class QueryCustomFieldColumn < QueryColumn
78
78
79 def initialize(custom_field)
79 def initialize(custom_field)
80 self.name = "cf_#{custom_field.id}".to_sym
80 self.name = "cf_#{custom_field.id}".to_sym
81 self.sortable = custom_field.order_statement || false
81 self.sortable = custom_field.order_statement || false
82 self.groupable = custom_field.group_statement || false
82 self.groupable = custom_field.group_statement || false
83 self.totalable = ['int', 'float'].include?(custom_field.field_format)
83 self.totalable = ['int', 'float'].include?(custom_field.field_format)
84 @inline = true
84 @inline = true
85 @cf = custom_field
85 @cf = custom_field
86 end
86 end
87
87
88 def caption
88 def caption
89 @cf.name
89 @cf.name
90 end
90 end
91
91
92 def custom_field
92 def custom_field
93 @cf
93 @cf
94 end
94 end
95
95
96 def value_object(object)
96 def value_object(object)
97 if custom_field.visible_by?(object.project, User.current)
97 if custom_field.visible_by?(object.project, User.current)
98 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
98 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
99 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
99 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
100 else
100 else
101 nil
101 nil
102 end
102 end
103 end
103 end
104
104
105 def value(object)
105 def value(object)
106 raw = value_object(object)
106 raw = value_object(object)
107 if raw.is_a?(Array)
107 if raw.is_a?(Array)
108 raw.map {|r| @cf.cast_value(r.value)}
108 raw.map {|r| @cf.cast_value(r.value)}
109 elsif raw
109 elsif raw
110 @cf.cast_value(raw.value)
110 @cf.cast_value(raw.value)
111 else
111 else
112 nil
112 nil
113 end
113 end
114 end
114 end
115
115
116 def css_classes
116 def css_classes
117 @css_classes ||= "#{name} #{@cf.field_format}"
117 @css_classes ||= "#{name} #{@cf.field_format}"
118 end
118 end
119 end
119 end
120
120
121 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
121 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
122
122
123 def initialize(association, custom_field)
123 def initialize(association, custom_field)
124 super(custom_field)
124 super(custom_field)
125 self.name = "#{association}.cf_#{custom_field.id}".to_sym
125 self.name = "#{association}.cf_#{custom_field.id}".to_sym
126 # TODO: support sorting/grouping by association custom field
126 # TODO: support sorting/grouping by association custom field
127 self.sortable = false
127 self.sortable = false
128 self.groupable = false
128 self.groupable = false
129 @association = association
129 @association = association
130 end
130 end
131
131
132 def value_object(object)
132 def value_object(object)
133 if assoc = object.send(@association)
133 if assoc = object.send(@association)
134 super(assoc)
134 super(assoc)
135 end
135 end
136 end
136 end
137
137
138 def css_classes
138 def css_classes
139 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
139 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
140 end
140 end
141 end
141 end
142
142
143 class Query < ActiveRecord::Base
143 class Query < ActiveRecord::Base
144 class StatementInvalid < ::ActiveRecord::StatementInvalid
144 class StatementInvalid < ::ActiveRecord::StatementInvalid
145 end
145 end
146
146
147 VISIBILITY_PRIVATE = 0
147 VISIBILITY_PRIVATE = 0
148 VISIBILITY_ROLES = 1
148 VISIBILITY_ROLES = 1
149 VISIBILITY_PUBLIC = 2
149 VISIBILITY_PUBLIC = 2
150
150
151 belongs_to :project
151 belongs_to :project
152 belongs_to :user
152 belongs_to :user
153 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
153 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
154 serialize :filters
154 serialize :filters
155 serialize :column_names
155 serialize :column_names
156 serialize :sort_criteria, Array
156 serialize :sort_criteria, Array
157 serialize :options, Hash
157 serialize :options, Hash
158
158
159 attr_protected :project_id, :user_id
159 attr_protected :project_id, :user_id
160
160
161 validates_presence_of :name
161 validates_presence_of :name
162 validates_length_of :name, :maximum => 255
162 validates_length_of :name, :maximum => 255
163 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
163 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
164 validate :validate_query_filters
164 validate :validate_query_filters
165 validate do |query|
165 validate do |query|
166 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
166 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
167 end
167 end
168
168
169 after_save do |query|
169 after_save do |query|
170 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
170 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
171 query.roles.clear
171 query.roles.clear
172 end
172 end
173 end
173 end
174
174
175 class_attribute :operators
175 class_attribute :operators
176 self.operators = {
176 self.operators = {
177 "=" => :label_equals,
177 "=" => :label_equals,
178 "!" => :label_not_equals,
178 "!" => :label_not_equals,
179 "o" => :label_open_issues,
179 "o" => :label_open_issues,
180 "c" => :label_closed_issues,
180 "c" => :label_closed_issues,
181 "!*" => :label_none,
181 "!*" => :label_none,
182 "*" => :label_any,
182 "*" => :label_any,
183 ">=" => :label_greater_or_equal,
183 ">=" => :label_greater_or_equal,
184 "<=" => :label_less_or_equal,
184 "<=" => :label_less_or_equal,
185 "><" => :label_between,
185 "><" => :label_between,
186 "<t+" => :label_in_less_than,
186 "<t+" => :label_in_less_than,
187 ">t+" => :label_in_more_than,
187 ">t+" => :label_in_more_than,
188 "><t+"=> :label_in_the_next_days,
188 "><t+"=> :label_in_the_next_days,
189 "t+" => :label_in,
189 "t+" => :label_in,
190 "t" => :label_today,
190 "t" => :label_today,
191 "ld" => :label_yesterday,
191 "ld" => :label_yesterday,
192 "w" => :label_this_week,
192 "w" => :label_this_week,
193 "lw" => :label_last_week,
193 "lw" => :label_last_week,
194 "l2w" => [:label_last_n_weeks, {:count => 2}],
194 "l2w" => [:label_last_n_weeks, {:count => 2}],
195 "m" => :label_this_month,
195 "m" => :label_this_month,
196 "lm" => :label_last_month,
196 "lm" => :label_last_month,
197 "y" => :label_this_year,
197 "y" => :label_this_year,
198 ">t-" => :label_less_than_ago,
198 ">t-" => :label_less_than_ago,
199 "<t-" => :label_more_than_ago,
199 "<t-" => :label_more_than_ago,
200 "><t-"=> :label_in_the_past_days,
200 "><t-"=> :label_in_the_past_days,
201 "t-" => :label_ago,
201 "t-" => :label_ago,
202 "~" => :label_contains,
202 "~" => :label_contains,
203 "!~" => :label_not_contains,
203 "!~" => :label_not_contains,
204 "=p" => :label_any_issues_in_project,
204 "=p" => :label_any_issues_in_project,
205 "=!p" => :label_any_issues_not_in_project,
205 "=!p" => :label_any_issues_not_in_project,
206 "!p" => :label_no_issues_in_project
206 "!p" => :label_no_issues_in_project
207 }
207 }
208
208
209 class_attribute :operators_by_filter_type
209 class_attribute :operators_by_filter_type
210 self.operators_by_filter_type = {
210 self.operators_by_filter_type = {
211 :list => [ "=", "!" ],
211 :list => [ "=", "!" ],
212 :list_status => [ "o", "=", "!", "c", "*" ],
212 :list_status => [ "o", "=", "!", "c", "*" ],
213 :list_optional => [ "=", "!", "!*", "*" ],
213 :list_optional => [ "=", "!", "!*", "*" ],
214 :list_subprojects => [ "*", "!*", "=" ],
214 :list_subprojects => [ "*", "!*", "=" ],
215 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
215 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
216 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
216 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
217 :string => [ "=", "~", "!", "!~", "!*", "*" ],
217 :string => [ "=", "~", "!", "!~", "!*", "*" ],
218 :text => [ "~", "!~", "!*", "*" ],
218 :text => [ "~", "!~", "!*", "*" ],
219 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
219 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
220 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
220 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
221 :relation => ["=", "=p", "=!p", "!p", "!*", "*"],
221 :relation => ["=", "=p", "=!p", "!p", "!*", "*"],
222 :tree => ["=", "~", "!*", "*"]
222 :tree => ["=", "~", "!*", "*"]
223 }
223 }
224
224
225 class_attribute :available_columns
225 class_attribute :available_columns
226 self.available_columns = []
226 self.available_columns = []
227
227
228 class_attribute :queried_class
228 class_attribute :queried_class
229
229
230 def queried_table_name
230 def queried_table_name
231 @queried_table_name ||= self.class.queried_class.table_name
231 @queried_table_name ||= self.class.queried_class.table_name
232 end
232 end
233
233
234 def initialize(attributes=nil, *args)
234 def initialize(attributes=nil, *args)
235 super attributes
235 super attributes
236 @is_for_all = project.nil?
236 @is_for_all = project.nil?
237 end
237 end
238
238
239 # Builds the query from the given params
239 # Builds the query from the given params
240 def build_from_params(params)
240 def build_from_params(params)
241 if params[:fields] || params[:f]
241 if params[:fields] || params[:f]
242 self.filters = {}
242 self.filters = {}
243 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
243 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
244 else
244 else
245 available_filters.keys.each do |field|
245 available_filters.keys.each do |field|
246 add_short_filter(field, params[field]) if params[field]
246 add_short_filter(field, params[field]) if params[field]
247 end
247 end
248 end
248 end
249 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
249 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
250 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
250 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
251 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
251 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
252 self
252 self
253 end
253 end
254
254
255 # Builds a new query from the given params and attributes
255 # Builds a new query from the given params and attributes
256 def self.build_from_params(params, attributes={})
256 def self.build_from_params(params, attributes={})
257 new(attributes).build_from_params(params)
257 new(attributes).build_from_params(params)
258 end
258 end
259
259
260 def validate_query_filters
260 def validate_query_filters
261 filters.each_key do |field|
261 filters.each_key do |field|
262 if values_for(field)
262 if values_for(field)
263 case type_for(field)
263 case type_for(field)
264 when :integer
264 when :integer
265 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
265 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
266 when :float
266 when :float
267 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
267 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
268 when :date, :date_past
268 when :date, :date_past
269 case operator_for(field)
269 case operator_for(field)
270 when "=", ">=", "<=", "><"
270 when "=", ">=", "<=", "><"
271 add_filter_error(field, :invalid) if values_for(field).detect {|v|
271 add_filter_error(field, :invalid) if values_for(field).detect {|v|
272 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
272 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
273 }
273 }
274 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
274 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
275 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
275 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
276 end
276 end
277 end
277 end
278 end
278 end
279
279
280 add_filter_error(field, :blank) unless
280 add_filter_error(field, :blank) unless
281 # filter requires one or more values
281 # filter requires one or more values
282 (values_for(field) and !values_for(field).first.blank?) or
282 (values_for(field) and !values_for(field).first.blank?) or
283 # filter doesn't require any value
283 # filter doesn't require any value
284 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
284 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
285 end if filters
285 end if filters
286 end
286 end
287
287
288 def add_filter_error(field, message)
288 def add_filter_error(field, message)
289 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
289 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
290 errors.add(:base, m)
290 errors.add(:base, m)
291 end
291 end
292
292
293 def editable_by?(user)
293 def editable_by?(user)
294 return false unless user
294 return false unless user
295 # Admin can edit them all and regular users can edit their private queries
295 # Admin can edit them all and regular users can edit their private queries
296 return true if user.admin? || (is_private? && self.user_id == user.id)
296 return true if user.admin? || (is_private? && self.user_id == user.id)
297 # Members can not edit public queries that are for all project (only admin is allowed to)
297 # Members can not edit public queries that are for all project (only admin is allowed to)
298 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
298 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
299 end
299 end
300
300
301 def trackers
301 def trackers
302 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
302 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
303 end
303 end
304
304
305 # Returns a hash of localized labels for all filter operators
305 # Returns a hash of localized labels for all filter operators
306 def self.operators_labels
306 def self.operators_labels
307 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
307 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
308 end
308 end
309
309
310 # Returns a representation of the available filters for JSON serialization
310 # Returns a representation of the available filters for JSON serialization
311 def available_filters_as_json
311 def available_filters_as_json
312 json = {}
312 json = {}
313 available_filters.each do |field, options|
313 available_filters.each do |field, options|
314 json[field] = options.slice(:type, :name, :values).stringify_keys
314 options = options.slice(:type, :name, :values)
315 if options[:values] && values_for(field)
316 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
317 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
318 options[:values] += send(method, missing)
319 end
320 end
321 json[field] = options.stringify_keys
315 end
322 end
316 json
323 json
317 end
324 end
318
325
319 def all_projects
326 def all_projects
320 @all_projects ||= Project.visible.to_a
327 @all_projects ||= Project.visible.to_a
321 end
328 end
322
329
323 def all_projects_values
330 def all_projects_values
324 return @all_projects_values if @all_projects_values
331 return @all_projects_values if @all_projects_values
325
332
326 values = []
333 values = []
327 Project.project_tree(all_projects) do |p, level|
334 Project.project_tree(all_projects) do |p, level|
328 prefix = (level > 0 ? ('--' * level + ' ') : '')
335 prefix = (level > 0 ? ('--' * level + ' ') : '')
329 values << ["#{prefix}#{p.name}", p.id.to_s]
336 values << ["#{prefix}#{p.name}", p.id.to_s]
330 end
337 end
331 @all_projects_values = values
338 @all_projects_values = values
332 end
339 end
333
340
334 # Adds available filters
341 # Adds available filters
335 def initialize_available_filters
342 def initialize_available_filters
336 # implemented by sub-classes
343 # implemented by sub-classes
337 end
344 end
338 protected :initialize_available_filters
345 protected :initialize_available_filters
339
346
340 # Adds an available filter
347 # Adds an available filter
341 def add_available_filter(field, options)
348 def add_available_filter(field, options)
342 @available_filters ||= ActiveSupport::OrderedHash.new
349 @available_filters ||= ActiveSupport::OrderedHash.new
343 @available_filters[field] = options
350 @available_filters[field] = options
344 @available_filters
351 @available_filters
345 end
352 end
346
353
347 # Removes an available filter
354 # Removes an available filter
348 def delete_available_filter(field)
355 def delete_available_filter(field)
349 if @available_filters
356 if @available_filters
350 @available_filters.delete(field)
357 @available_filters.delete(field)
351 end
358 end
352 end
359 end
353
360
354 # Return a hash of available filters
361 # Return a hash of available filters
355 def available_filters
362 def available_filters
356 unless @available_filters
363 unless @available_filters
357 initialize_available_filters
364 initialize_available_filters
358 @available_filters.each do |field, options|
365 @available_filters.each do |field, options|
359 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
366 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
360 end
367 end
361 end
368 end
362 @available_filters
369 @available_filters
363 end
370 end
364
371
365 def add_filter(field, operator, values=nil)
372 def add_filter(field, operator, values=nil)
366 # values must be an array
373 # values must be an array
367 return unless values.nil? || values.is_a?(Array)
374 return unless values.nil? || values.is_a?(Array)
368 # check if field is defined as an available filter
375 # check if field is defined as an available filter
369 if available_filters.has_key? field
376 if available_filters.has_key? field
370 filter_options = available_filters[field]
377 filter_options = available_filters[field]
371 filters[field] = {:operator => operator, :values => (values || [''])}
378 filters[field] = {:operator => operator, :values => (values || [''])}
372 end
379 end
373 end
380 end
374
381
375 def add_short_filter(field, expression)
382 def add_short_filter(field, expression)
376 return unless expression && available_filters.has_key?(field)
383 return unless expression && available_filters.has_key?(field)
377 field_type = available_filters[field][:type]
384 field_type = available_filters[field][:type]
378 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
385 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
379 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
386 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
380 values = $1
387 values = $1
381 add_filter field, operator, values.present? ? values.split('|') : ['']
388 add_filter field, operator, values.present? ? values.split('|') : ['']
382 end || add_filter(field, '=', expression.split('|'))
389 end || add_filter(field, '=', expression.split('|'))
383 end
390 end
384
391
385 # Add multiple filters using +add_filter+
392 # Add multiple filters using +add_filter+
386 def add_filters(fields, operators, values)
393 def add_filters(fields, operators, values)
387 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
394 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
388 fields.each do |field|
395 fields.each do |field|
389 add_filter(field, operators[field], values && values[field])
396 add_filter(field, operators[field], values && values[field])
390 end
397 end
391 end
398 end
392 end
399 end
393
400
394 def has_filter?(field)
401 def has_filter?(field)
395 filters and filters[field]
402 filters and filters[field]
396 end
403 end
397
404
398 def type_for(field)
405 def type_for(field)
399 available_filters[field][:type] if available_filters.has_key?(field)
406 available_filters[field][:type] if available_filters.has_key?(field)
400 end
407 end
401
408
402 def operator_for(field)
409 def operator_for(field)
403 has_filter?(field) ? filters[field][:operator] : nil
410 has_filter?(field) ? filters[field][:operator] : nil
404 end
411 end
405
412
406 def values_for(field)
413 def values_for(field)
407 has_filter?(field) ? filters[field][:values] : nil
414 has_filter?(field) ? filters[field][:values] : nil
408 end
415 end
409
416
410 def value_for(field, index=0)
417 def value_for(field, index=0)
411 (values_for(field) || [])[index]
418 (values_for(field) || [])[index]
412 end
419 end
413
420
414 def label_for(field)
421 def label_for(field)
415 label = available_filters[field][:name] if available_filters.has_key?(field)
422 label = available_filters[field][:name] if available_filters.has_key?(field)
416 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
423 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
417 end
424 end
418
425
419 def self.add_available_column(column)
426 def self.add_available_column(column)
420 self.available_columns << (column) if column.is_a?(QueryColumn)
427 self.available_columns << (column) if column.is_a?(QueryColumn)
421 end
428 end
422
429
423 # Returns an array of columns that can be used to group the results
430 # Returns an array of columns that can be used to group the results
424 def groupable_columns
431 def groupable_columns
425 available_columns.select {|c| c.groupable}
432 available_columns.select {|c| c.groupable}
426 end
433 end
427
434
428 # Returns a Hash of columns and the key for sorting
435 # Returns a Hash of columns and the key for sorting
429 def sortable_columns
436 def sortable_columns
430 available_columns.inject({}) {|h, column|
437 available_columns.inject({}) {|h, column|
431 h[column.name.to_s] = column.sortable
438 h[column.name.to_s] = column.sortable
432 h
439 h
433 }
440 }
434 end
441 end
435
442
436 def columns
443 def columns
437 # preserve the column_names order
444 # preserve the column_names order
438 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
445 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
439 available_columns.find { |col| col.name == name }
446 available_columns.find { |col| col.name == name }
440 end.compact
447 end.compact
441 available_columns.select(&:frozen?) | cols
448 available_columns.select(&:frozen?) | cols
442 end
449 end
443
450
444 def inline_columns
451 def inline_columns
445 columns.select(&:inline?)
452 columns.select(&:inline?)
446 end
453 end
447
454
448 def block_columns
455 def block_columns
449 columns.reject(&:inline?)
456 columns.reject(&:inline?)
450 end
457 end
451
458
452 def available_inline_columns
459 def available_inline_columns
453 available_columns.select(&:inline?)
460 available_columns.select(&:inline?)
454 end
461 end
455
462
456 def available_block_columns
463 def available_block_columns
457 available_columns.reject(&:inline?)
464 available_columns.reject(&:inline?)
458 end
465 end
459
466
460 def available_totalable_columns
467 def available_totalable_columns
461 available_columns.select(&:totalable)
468 available_columns.select(&:totalable)
462 end
469 end
463
470
464 def default_columns_names
471 def default_columns_names
465 []
472 []
466 end
473 end
467
474
468 def column_names=(names)
475 def column_names=(names)
469 if names
476 if names
470 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
477 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
471 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
478 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
472 # Set column_names to nil if default columns
479 # Set column_names to nil if default columns
473 if names == default_columns_names
480 if names == default_columns_names
474 names = nil
481 names = nil
475 end
482 end
476 end
483 end
477 write_attribute(:column_names, names)
484 write_attribute(:column_names, names)
478 end
485 end
479
486
480 def has_column?(column)
487 def has_column?(column)
481 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
488 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
482 end
489 end
483
490
484 def has_custom_field_column?
491 def has_custom_field_column?
485 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
492 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
486 end
493 end
487
494
488 def has_default_columns?
495 def has_default_columns?
489 column_names.nil? || column_names.empty?
496 column_names.nil? || column_names.empty?
490 end
497 end
491
498
492 def totalable_columns
499 def totalable_columns
493 names = totalable_names
500 names = totalable_names
494 available_totalable_columns.select {|column| names.include?(column.name)}
501 available_totalable_columns.select {|column| names.include?(column.name)}
495 end
502 end
496
503
497 def totalable_names=(names)
504 def totalable_names=(names)
498 if names
505 if names
499 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
506 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
500 end
507 end
501 options[:totalable_names] = names
508 options[:totalable_names] = names
502 end
509 end
503
510
504 def totalable_names
511 def totalable_names
505 options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || []
512 options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || []
506 end
513 end
507
514
508 def sort_criteria=(arg)
515 def sort_criteria=(arg)
509 c = []
516 c = []
510 if arg.is_a?(Hash)
517 if arg.is_a?(Hash)
511 arg = arg.keys.sort.collect {|k| arg[k]}
518 arg = arg.keys.sort.collect {|k| arg[k]}
512 end
519 end
513 if arg
520 if arg
514 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
521 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
515 end
522 end
516 write_attribute(:sort_criteria, c)
523 write_attribute(:sort_criteria, c)
517 end
524 end
518
525
519 def sort_criteria
526 def sort_criteria
520 read_attribute(:sort_criteria) || []
527 read_attribute(:sort_criteria) || []
521 end
528 end
522
529
523 def sort_criteria_key(arg)
530 def sort_criteria_key(arg)
524 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
531 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
525 end
532 end
526
533
527 def sort_criteria_order(arg)
534 def sort_criteria_order(arg)
528 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
535 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
529 end
536 end
530
537
531 def sort_criteria_order_for(key)
538 def sort_criteria_order_for(key)
532 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
539 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
533 end
540 end
534
541
535 # Returns the SQL sort order that should be prepended for grouping
542 # Returns the SQL sort order that should be prepended for grouping
536 def group_by_sort_order
543 def group_by_sort_order
537 if grouped? && (column = group_by_column)
544 if grouped? && (column = group_by_column)
538 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
545 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
539 column.sortable.is_a?(Array) ?
546 column.sortable.is_a?(Array) ?
540 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
547 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
541 "#{column.sortable} #{order}"
548 "#{column.sortable} #{order}"
542 end
549 end
543 end
550 end
544
551
545 # Returns true if the query is a grouped query
552 # Returns true if the query is a grouped query
546 def grouped?
553 def grouped?
547 !group_by_column.nil?
554 !group_by_column.nil?
548 end
555 end
549
556
550 def group_by_column
557 def group_by_column
551 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
558 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
552 end
559 end
553
560
554 def group_by_statement
561 def group_by_statement
555 group_by_column.try(:groupable)
562 group_by_column.try(:groupable)
556 end
563 end
557
564
558 def project_statement
565 def project_statement
559 project_clauses = []
566 project_clauses = []
560 if project && !project.descendants.active.empty?
567 if project && !project.descendants.active.empty?
561 ids = [project.id]
568 ids = [project.id]
562 if has_filter?("subproject_id")
569 if has_filter?("subproject_id")
563 case operator_for("subproject_id")
570 case operator_for("subproject_id")
564 when '='
571 when '='
565 # include the selected subprojects
572 # include the selected subprojects
566 ids += values_for("subproject_id").each(&:to_i)
573 ids += values_for("subproject_id").each(&:to_i)
567 when '!*'
574 when '!*'
568 # main project only
575 # main project only
569 else
576 else
570 # all subprojects
577 # all subprojects
571 ids += project.descendants.collect(&:id)
578 ids += project.descendants.collect(&:id)
572 end
579 end
573 elsif Setting.display_subprojects_issues?
580 elsif Setting.display_subprojects_issues?
574 ids += project.descendants.collect(&:id)
581 ids += project.descendants.collect(&:id)
575 end
582 end
576 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
583 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
577 elsif project
584 elsif project
578 project_clauses << "#{Project.table_name}.id = %d" % project.id
585 project_clauses << "#{Project.table_name}.id = %d" % project.id
579 end
586 end
580 project_clauses.any? ? project_clauses.join(' AND ') : nil
587 project_clauses.any? ? project_clauses.join(' AND ') : nil
581 end
588 end
582
589
583 def statement
590 def statement
584 # filters clauses
591 # filters clauses
585 filters_clauses = []
592 filters_clauses = []
586 filters.each_key do |field|
593 filters.each_key do |field|
587 next if field == "subproject_id"
594 next if field == "subproject_id"
588 v = values_for(field).clone
595 v = values_for(field).clone
589 next unless v and !v.empty?
596 next unless v and !v.empty?
590 operator = operator_for(field)
597 operator = operator_for(field)
591
598
592 # "me" value substitution
599 # "me" value substitution
593 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
600 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
594 if v.delete("me")
601 if v.delete("me")
595 if User.current.logged?
602 if User.current.logged?
596 v.push(User.current.id.to_s)
603 v.push(User.current.id.to_s)
597 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
604 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
598 else
605 else
599 v.push("0")
606 v.push("0")
600 end
607 end
601 end
608 end
602 end
609 end
603
610
604 if field == 'project_id'
611 if field == 'project_id'
605 if v.delete('mine')
612 if v.delete('mine')
606 v += User.current.memberships.map(&:project_id).map(&:to_s)
613 v += User.current.memberships.map(&:project_id).map(&:to_s)
607 end
614 end
608 end
615 end
609
616
610 if field =~ /cf_(\d+)$/
617 if field =~ /cf_(\d+)$/
611 # custom field
618 # custom field
612 filters_clauses << sql_for_custom_field(field, operator, v, $1)
619 filters_clauses << sql_for_custom_field(field, operator, v, $1)
613 elsif respond_to?("sql_for_#{field}_field")
620 elsif respond_to?("sql_for_#{field}_field")
614 # specific statement
621 # specific statement
615 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
622 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
616 else
623 else
617 # regular field
624 # regular field
618 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
625 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
619 end
626 end
620 end if filters and valid?
627 end if filters and valid?
621
628
622 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
629 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
623 # Excludes results for which the grouped custom field is not visible
630 # Excludes results for which the grouped custom field is not visible
624 filters_clauses << c.custom_field.visibility_by_project_condition
631 filters_clauses << c.custom_field.visibility_by_project_condition
625 end
632 end
626
633
627 filters_clauses << project_statement
634 filters_clauses << project_statement
628 filters_clauses.reject!(&:blank?)
635 filters_clauses.reject!(&:blank?)
629
636
630 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
637 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
631 end
638 end
632
639
633 # Returns the sum of values for the given column
640 # Returns the sum of values for the given column
634 def total_for(column)
641 def total_for(column)
635 total_with_scope(column, base_scope)
642 total_with_scope(column, base_scope)
636 end
643 end
637
644
638 # Returns a hash of the sum of the given column for each group,
645 # Returns a hash of the sum of the given column for each group,
639 # or nil if the query is not grouped
646 # or nil if the query is not grouped
640 def total_by_group_for(column)
647 def total_by_group_for(column)
641 grouped_query do |scope|
648 grouped_query do |scope|
642 total_with_scope(column, scope)
649 total_with_scope(column, scope)
643 end
650 end
644 end
651 end
645
652
646 def totals
653 def totals
647 totals = totalable_columns.map {|column| [column, total_for(column)]}
654 totals = totalable_columns.map {|column| [column, total_for(column)]}
648 yield totals if block_given?
655 yield totals if block_given?
649 totals
656 totals
650 end
657 end
651
658
652 def totals_by_group
659 def totals_by_group
653 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
660 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
654 yield totals if block_given?
661 yield totals if block_given?
655 totals
662 totals
656 end
663 end
657
664
658 private
665 private
659
666
660 def grouped_query(&block)
667 def grouped_query(&block)
661 r = nil
668 r = nil
662 if grouped?
669 if grouped?
663 begin
670 begin
664 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
671 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
665 r = yield base_group_scope
672 r = yield base_group_scope
666 rescue ActiveRecord::RecordNotFound
673 rescue ActiveRecord::RecordNotFound
667 r = {nil => yield(base_scope)}
674 r = {nil => yield(base_scope)}
668 end
675 end
669 c = group_by_column
676 c = group_by_column
670 if c.is_a?(QueryCustomFieldColumn)
677 if c.is_a?(QueryCustomFieldColumn)
671 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
678 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
672 end
679 end
673 end
680 end
674 r
681 r
675 rescue ::ActiveRecord::StatementInvalid => e
682 rescue ::ActiveRecord::StatementInvalid => e
676 raise StatementInvalid.new(e.message)
683 raise StatementInvalid.new(e.message)
677 end
684 end
678
685
679 def total_with_scope(column, scope)
686 def total_with_scope(column, scope)
680 unless column.is_a?(QueryColumn)
687 unless column.is_a?(QueryColumn)
681 column = column.to_sym
688 column = column.to_sym
682 column = available_totalable_columns.detect {|c| c.name == column}
689 column = available_totalable_columns.detect {|c| c.name == column}
683 end
690 end
684 if column.is_a?(QueryCustomFieldColumn)
691 if column.is_a?(QueryCustomFieldColumn)
685 custom_field = column.custom_field
692 custom_field = column.custom_field
686 send "total_for_#{custom_field.field_format}_custom_field", custom_field, scope
693 send "total_for_#{custom_field.field_format}_custom_field", custom_field, scope
687 else
694 else
688 send "total_for_#{column.name}", scope
695 send "total_for_#{column.name}", scope
689 end
696 end
690 rescue ::ActiveRecord::StatementInvalid => e
697 rescue ::ActiveRecord::StatementInvalid => e
691 raise StatementInvalid.new(e.message)
698 raise StatementInvalid.new(e.message)
692 end
699 end
693
700
694 def base_scope
701 def base_scope
695 raise "unimplemented"
702 raise "unimplemented"
696 end
703 end
697
704
698 def base_group_scope
705 def base_group_scope
699 base_scope.
706 base_scope.
700 joins(joins_for_order_statement(group_by_statement)).
707 joins(joins_for_order_statement(group_by_statement)).
701 group(group_by_statement)
708 group(group_by_statement)
702 end
709 end
703
710
704 def total_for_float_custom_field(custom_field, scope)
711 def total_for_float_custom_field(custom_field, scope)
705 total_for_custom_field(custom_field, scope) {|t| t.to_f.round(2)}
712 total_for_custom_field(custom_field, scope) {|t| t.to_f.round(2)}
706 end
713 end
707
714
708 def total_for_int_custom_field(custom_field, scope)
715 def total_for_int_custom_field(custom_field, scope)
709 total_for_custom_field(custom_field, scope) {|t| t.to_i}
716 total_for_custom_field(custom_field, scope) {|t| t.to_i}
710 end
717 end
711
718
712 def total_for_custom_field(custom_field, scope, &block)
719 def total_for_custom_field(custom_field, scope, &block)
713 total = scope.joins(:custom_values).
720 total = scope.joins(:custom_values).
714 where(:custom_values => {:custom_field_id => custom_field.id}).
721 where(:custom_values => {:custom_field_id => custom_field.id}).
715 where.not(:custom_values => {:value => ''}).
722 where.not(:custom_values => {:value => ''}).
716 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
723 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
717
724
718 total = map_total(total, &block) if block_given?
725 total = map_total(total, &block) if block_given?
719 total
726 total
720 end
727 end
721
728
722 def map_total(total, &block)
729 def map_total(total, &block)
723 if total.is_a?(Hash)
730 if total.is_a?(Hash)
724 total.keys.each {|k| total[k] = yield total[k]}
731 total.keys.each {|k| total[k] = yield total[k]}
725 else
732 else
726 total = yield total
733 total = yield total
727 end
734 end
728 total
735 total
729 end
736 end
730
737
731 def sql_for_custom_field(field, operator, value, custom_field_id)
738 def sql_for_custom_field(field, operator, value, custom_field_id)
732 db_table = CustomValue.table_name
739 db_table = CustomValue.table_name
733 db_field = 'value'
740 db_field = 'value'
734 filter = @available_filters[field]
741 filter = @available_filters[field]
735 return nil unless filter
742 return nil unless filter
736 if filter[:field].format.target_class && filter[:field].format.target_class <= User
743 if filter[:field].format.target_class && filter[:field].format.target_class <= User
737 if value.delete('me')
744 if value.delete('me')
738 value.push User.current.id.to_s
745 value.push User.current.id.to_s
739 end
746 end
740 end
747 end
741 not_in = nil
748 not_in = nil
742 if operator == '!'
749 if operator == '!'
743 # Makes ! operator work for custom fields with multiple values
750 # Makes ! operator work for custom fields with multiple values
744 operator = '='
751 operator = '='
745 not_in = 'NOT'
752 not_in = 'NOT'
746 end
753 end
747 customized_key = "id"
754 customized_key = "id"
748 customized_class = queried_class
755 customized_class = queried_class
749 if field =~ /^(.+)\.cf_/
756 if field =~ /^(.+)\.cf_/
750 assoc = $1
757 assoc = $1
751 customized_key = "#{assoc}_id"
758 customized_key = "#{assoc}_id"
752 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
759 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
753 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
760 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
754 end
761 end
755 where = sql_for_field(field, operator, value, db_table, db_field, true)
762 where = sql_for_field(field, operator, value, db_table, db_field, true)
756 if operator =~ /[<>]/
763 if operator =~ /[<>]/
757 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
764 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
758 end
765 end
759 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
766 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
760 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
767 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
761 " 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}" +
768 " 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}" +
762 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
769 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
763 end
770 end
764
771
765 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
772 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
766 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
773 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
767 sql = ''
774 sql = ''
768 case operator
775 case operator
769 when "="
776 when "="
770 if value.any?
777 if value.any?
771 case type_for(field)
778 case type_for(field)
772 when :date, :date_past
779 when :date, :date_past
773 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
780 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
774 when :integer
781 when :integer
775 if is_custom_filter
782 if is_custom_filter
776 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
783 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
777 else
784 else
778 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
785 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
779 end
786 end
780 when :float
787 when :float
781 if is_custom_filter
788 if is_custom_filter
782 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
789 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
783 else
790 else
784 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
791 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
785 end
792 end
786 else
793 else
787 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
794 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
788 end
795 end
789 else
796 else
790 # IN an empty set
797 # IN an empty set
791 sql = "1=0"
798 sql = "1=0"
792 end
799 end
793 when "!"
800 when "!"
794 if value.any?
801 if value.any?
795 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + "))"
802 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + "))"
796 else
803 else
797 # NOT IN an empty set
804 # NOT IN an empty set
798 sql = "1=1"
805 sql = "1=1"
799 end
806 end
800 when "!*"
807 when "!*"
801 sql = "#{db_table}.#{db_field} IS NULL"
808 sql = "#{db_table}.#{db_field} IS NULL"
802 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
809 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
803 when "*"
810 when "*"
804 sql = "#{db_table}.#{db_field} IS NOT NULL"
811 sql = "#{db_table}.#{db_field} IS NOT NULL"
805 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
812 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
806 when ">="
813 when ">="
807 if [:date, :date_past].include?(type_for(field))
814 if [:date, :date_past].include?(type_for(field))
808 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
815 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
809 else
816 else
810 if is_custom_filter
817 if is_custom_filter
811 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
818 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
812 else
819 else
813 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
820 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
814 end
821 end
815 end
822 end
816 when "<="
823 when "<="
817 if [:date, :date_past].include?(type_for(field))
824 if [:date, :date_past].include?(type_for(field))
818 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
825 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
819 else
826 else
820 if is_custom_filter
827 if is_custom_filter
821 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
828 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
822 else
829 else
823 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
830 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
824 end
831 end
825 end
832 end
826 when "><"
833 when "><"
827 if [:date, :date_past].include?(type_for(field))
834 if [:date, :date_past].include?(type_for(field))
828 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
835 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
829 else
836 else
830 if is_custom_filter
837 if is_custom_filter
831 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
838 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
832 else
839 else
833 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
840 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
834 end
841 end
835 end
842 end
836 when "o"
843 when "o"
837 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
844 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
838 when "c"
845 when "c"
839 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
846 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
840 when "><t-"
847 when "><t-"
841 # between today - n days and today
848 # between today - n days and today
842 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
849 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
843 when ">t-"
850 when ">t-"
844 # >= today - n days
851 # >= today - n days
845 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
852 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
846 when "<t-"
853 when "<t-"
847 # <= today - n days
854 # <= today - n days
848 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
855 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
849 when "t-"
856 when "t-"
850 # = n days in past
857 # = n days in past
851 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
858 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
852 when "><t+"
859 when "><t+"
853 # between today and today + n days
860 # between today and today + n days
854 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
861 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
855 when ">t+"
862 when ">t+"
856 # >= today + n days
863 # >= today + n days
857 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
864 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
858 when "<t+"
865 when "<t+"
859 # <= today + n days
866 # <= today + n days
860 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
867 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
861 when "t+"
868 when "t+"
862 # = today + n days
869 # = today + n days
863 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
870 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
864 when "t"
871 when "t"
865 # = today
872 # = today
866 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
873 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
867 when "ld"
874 when "ld"
868 # = yesterday
875 # = yesterday
869 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
876 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
870 when "w"
877 when "w"
871 # = this week
878 # = this week
872 first_day_of_week = l(:general_first_day_of_week).to_i
879 first_day_of_week = l(:general_first_day_of_week).to_i
873 day_of_week = Date.today.cwday
880 day_of_week = Date.today.cwday
874 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
881 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
875 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
882 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
876 when "lw"
883 when "lw"
877 # = last week
884 # = last week
878 first_day_of_week = l(:general_first_day_of_week).to_i
885 first_day_of_week = l(:general_first_day_of_week).to_i
879 day_of_week = Date.today.cwday
886 day_of_week = Date.today.cwday
880 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
887 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
881 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
888 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
882 when "l2w"
889 when "l2w"
883 # = last 2 weeks
890 # = last 2 weeks
884 first_day_of_week = l(:general_first_day_of_week).to_i
891 first_day_of_week = l(:general_first_day_of_week).to_i
885 day_of_week = Date.today.cwday
892 day_of_week = Date.today.cwday
886 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
893 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
887 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
894 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
888 when "m"
895 when "m"
889 # = this month
896 # = this month
890 date = Date.today
897 date = Date.today
891 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
898 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
892 when "lm"
899 when "lm"
893 # = last month
900 # = last month
894 date = Date.today.prev_month
901 date = Date.today.prev_month
895 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
902 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
896 when "y"
903 when "y"
897 # = this year
904 # = this year
898 date = Date.today
905 date = Date.today
899 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
906 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
900 when "~"
907 when "~"
901 sql = sql_contains("#{db_table}.#{db_field}", value.first)
908 sql = sql_contains("#{db_table}.#{db_field}", value.first)
902 when "!~"
909 when "!~"
903 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
910 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
904 else
911 else
905 raise "Unknown query operator #{operator}"
912 raise "Unknown query operator #{operator}"
906 end
913 end
907
914
908 return sql
915 return sql
909 end
916 end
910
917
911 # Returns a SQL LIKE statement with wildcards
918 # Returns a SQL LIKE statement with wildcards
912 def sql_contains(db_field, value, match=true)
919 def sql_contains(db_field, value, match=true)
913 value = "'%#{self.class.connection.quote_string(value.to_s)}%'"
920 value = "'%#{self.class.connection.quote_string(value.to_s)}%'"
914 Redmine::Database.like(db_field, value, :match => match)
921 Redmine::Database.like(db_field, value, :match => match)
915 end
922 end
916
923
917 # Adds a filter for the given custom field
924 # Adds a filter for the given custom field
918 def add_custom_field_filter(field, assoc=nil)
925 def add_custom_field_filter(field, assoc=nil)
919 options = field.query_filter_options(self)
926 options = field.query_filter_options(self)
920 if field.format.target_class && field.format.target_class <= User
927 if field.format.target_class && field.format.target_class <= User
921 if options[:values].is_a?(Array) && User.current.logged?
928 if options[:values].is_a?(Array) && User.current.logged?
922 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
929 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
923 end
930 end
924 end
931 end
925
932
926 filter_id = "cf_#{field.id}"
933 filter_id = "cf_#{field.id}"
927 filter_name = field.name
934 filter_name = field.name
928 if assoc.present?
935 if assoc.present?
929 filter_id = "#{assoc}.#{filter_id}"
936 filter_id = "#{assoc}.#{filter_id}"
930 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
937 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
931 end
938 end
932 add_available_filter filter_id, options.merge({
939 add_available_filter filter_id, options.merge({
933 :name => filter_name,
940 :name => filter_name,
934 :field => field
941 :field => field
935 })
942 })
936 end
943 end
937
944
938 # Adds filters for the given custom fields scope
945 # Adds filters for the given custom fields scope
939 def add_custom_fields_filters(scope, assoc=nil)
946 def add_custom_fields_filters(scope, assoc=nil)
940 scope.visible.where(:is_filter => true).sorted.each do |field|
947 scope.visible.where(:is_filter => true).sorted.each do |field|
941 add_custom_field_filter(field, assoc)
948 add_custom_field_filter(field, assoc)
942 end
949 end
943 end
950 end
944
951
945 # Adds filters for the given associations custom fields
952 # Adds filters for the given associations custom fields
946 def add_associations_custom_fields_filters(*associations)
953 def add_associations_custom_fields_filters(*associations)
947 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
954 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
948 associations.each do |assoc|
955 associations.each do |assoc|
949 association_klass = queried_class.reflect_on_association(assoc).klass
956 association_klass = queried_class.reflect_on_association(assoc).klass
950 fields_by_class.each do |field_class, fields|
957 fields_by_class.each do |field_class, fields|
951 if field_class.customized_class <= association_klass
958 if field_class.customized_class <= association_klass
952 fields.sort.each do |field|
959 fields.sort.each do |field|
953 add_custom_field_filter(field, assoc)
960 add_custom_field_filter(field, assoc)
954 end
961 end
955 end
962 end
956 end
963 end
957 end
964 end
958 end
965 end
959
966
960 def quoted_time(time, is_custom_filter)
967 def quoted_time(time, is_custom_filter)
961 if is_custom_filter
968 if is_custom_filter
962 # Custom field values are stored as strings in the DB
969 # Custom field values are stored as strings in the DB
963 # using this format that does not depend on DB date representation
970 # using this format that does not depend on DB date representation
964 time.strftime("%Y-%m-%d %H:%M:%S")
971 time.strftime("%Y-%m-%d %H:%M:%S")
965 else
972 else
966 self.class.connection.quoted_date(time)
973 self.class.connection.quoted_date(time)
967 end
974 end
968 end
975 end
969
976
970 # Returns a SQL clause for a date or datetime field.
977 # Returns a SQL clause for a date or datetime field.
971 def date_clause(table, field, from, to, is_custom_filter)
978 def date_clause(table, field, from, to, is_custom_filter)
972 s = []
979 s = []
973 if from
980 if from
974 if from.is_a?(Date)
981 if from.is_a?(Date)
975 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
982 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
976 else
983 else
977 from = from - 1 # second
984 from = from - 1 # second
978 end
985 end
979 if self.class.default_timezone == :utc
986 if self.class.default_timezone == :utc
980 from = from.utc
987 from = from.utc
981 end
988 end
982 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
989 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
983 end
990 end
984 if to
991 if to
985 if to.is_a?(Date)
992 if to.is_a?(Date)
986 to = Time.local(to.year, to.month, to.day).end_of_day
993 to = Time.local(to.year, to.month, to.day).end_of_day
987 end
994 end
988 if self.class.default_timezone == :utc
995 if self.class.default_timezone == :utc
989 to = to.utc
996 to = to.utc
990 end
997 end
991 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
998 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
992 end
999 end
993 s.join(' AND ')
1000 s.join(' AND ')
994 end
1001 end
995
1002
996 # Returns a SQL clause for a date or datetime field using relative dates.
1003 # Returns a SQL clause for a date or datetime field using relative dates.
997 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1004 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
998 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter)
1005 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter)
999 end
1006 end
1000
1007
1001 # Returns a Date or Time from the given filter value
1008 # Returns a Date or Time from the given filter value
1002 def parse_date(arg)
1009 def parse_date(arg)
1003 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1010 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1004 Time.parse(arg) rescue nil
1011 Time.parse(arg) rescue nil
1005 else
1012 else
1006 Date.parse(arg) rescue nil
1013 Date.parse(arg) rescue nil
1007 end
1014 end
1008 end
1015 end
1009
1016
1010 # Additional joins required for the given sort options
1017 # Additional joins required for the given sort options
1011 def joins_for_order_statement(order_options)
1018 def joins_for_order_statement(order_options)
1012 joins = []
1019 joins = []
1013
1020
1014 if order_options
1021 if order_options
1015 if order_options.include?('authors')
1022 if order_options.include?('authors')
1016 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1023 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1017 end
1024 end
1018 order_options.scan(/cf_\d+/).uniq.each do |name|
1025 order_options.scan(/cf_\d+/).uniq.each do |name|
1019 column = available_columns.detect {|c| c.name.to_s == name}
1026 column = available_columns.detect {|c| c.name.to_s == name}
1020 join = column && column.custom_field.join_for_order_statement
1027 join = column && column.custom_field.join_for_order_statement
1021 if join
1028 if join
1022 joins << join
1029 joins << join
1023 end
1030 end
1024 end
1031 end
1025 end
1032 end
1026
1033
1027 joins.any? ? joins.join(' ') : nil
1034 joins.any? ? joins.join(' ') : nil
1028 end
1035 end
1029 end
1036 end
@@ -1,1648 +1,1670
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 fixtures :projects, :enabled_modules, :users, :members,
23 fixtures :projects, :enabled_modules, :users, :members,
24 :member_roles, :roles, :trackers, :issue_statuses,
24 :member_roles, :roles, :trackers, :issue_statuses,
25 :issue_categories, :enumerations, :issues,
25 :issue_categories, :enumerations, :issues,
26 :watchers, :custom_fields, :custom_values, :versions,
26 :watchers, :custom_fields, :custom_values, :versions,
27 :queries,
27 :queries,
28 :projects_trackers,
28 :projects_trackers,
29 :custom_fields_trackers,
29 :custom_fields_trackers,
30 :workflows
30 :workflows
31
31
32 def setup
32 def setup
33 User.current = nil
33 User.current = nil
34 end
34 end
35
35
36 def test_query_with_roles_visibility_should_validate_roles
36 def test_query_with_roles_visibility_should_validate_roles
37 set_language_if_valid 'en'
37 set_language_if_valid 'en'
38 query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES)
38 query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES)
39 assert !query.save
39 assert !query.save
40 assert_include "Roles cannot be blank", query.errors.full_messages
40 assert_include "Roles cannot be blank", query.errors.full_messages
41 query.role_ids = [1, 2]
41 query.role_ids = [1, 2]
42 assert query.save
42 assert query.save
43 end
43 end
44
44
45 def test_changing_roles_visibility_should_clear_roles
45 def test_changing_roles_visibility_should_clear_roles
46 query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2])
46 query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2])
47 assert_equal 2, query.roles.count
47 assert_equal 2, query.roles.count
48
48
49 query.visibility = IssueQuery::VISIBILITY_PUBLIC
49 query.visibility = IssueQuery::VISIBILITY_PUBLIC
50 query.save!
50 query.save!
51 assert_equal 0, query.roles.count
51 assert_equal 0, query.roles.count
52 end
52 end
53
53
54 def test_available_filters_should_be_ordered
54 def test_available_filters_should_be_ordered
55 set_language_if_valid 'en'
55 set_language_if_valid 'en'
56 query = IssueQuery.new
56 query = IssueQuery.new
57 assert_equal 0, query.available_filters.keys.index('status_id')
57 assert_equal 0, query.available_filters.keys.index('status_id')
58 expected_order = [
58 expected_order = [
59 "Status",
59 "Status",
60 "Project",
60 "Project",
61 "Tracker",
61 "Tracker",
62 "Priority"
62 "Priority"
63 ]
63 ]
64 assert_equal expected_order,
64 assert_equal expected_order,
65 (query.available_filters.values.map{|v| v[:name]} & expected_order)
65 (query.available_filters.values.map{|v| v[:name]} & expected_order)
66 end
66 end
67
67
68 def test_available_filters_with_custom_fields_should_be_ordered
68 def test_available_filters_with_custom_fields_should_be_ordered
69 set_language_if_valid 'en'
69 set_language_if_valid 'en'
70 UserCustomField.create!(
70 UserCustomField.create!(
71 :name => 'order test', :field_format => 'string',
71 :name => 'order test', :field_format => 'string',
72 :is_for_all => true, :is_filter => true
72 :is_for_all => true, :is_filter => true
73 )
73 )
74 query = IssueQuery.new
74 query = IssueQuery.new
75 expected_order = [
75 expected_order = [
76 "Searchable field",
76 "Searchable field",
77 "Database",
77 "Database",
78 "Project's Development status",
78 "Project's Development status",
79 "Author's order test",
79 "Author's order test",
80 "Assignee's order test"
80 "Assignee's order test"
81 ]
81 ]
82 assert_equal expected_order,
82 assert_equal expected_order,
83 (query.available_filters.values.map{|v| v[:name]} & expected_order)
83 (query.available_filters.values.map{|v| v[:name]} & expected_order)
84 end
84 end
85
85
86 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
86 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
87 query = IssueQuery.new(:project => nil, :name => '_')
87 query = IssueQuery.new(:project => nil, :name => '_')
88 assert query.available_filters.has_key?('cf_1')
88 assert query.available_filters.has_key?('cf_1')
89 assert !query.available_filters.has_key?('cf_3')
89 assert !query.available_filters.has_key?('cf_3')
90 end
90 end
91
91
92 def test_system_shared_versions_should_be_available_in_global_queries
92 def test_system_shared_versions_should_be_available_in_global_queries
93 Version.find(2).update_attribute :sharing, 'system'
93 Version.find(2).update_attribute :sharing, 'system'
94 query = IssueQuery.new(:project => nil, :name => '_')
94 query = IssueQuery.new(:project => nil, :name => '_')
95 assert query.available_filters.has_key?('fixed_version_id')
95 assert query.available_filters.has_key?('fixed_version_id')
96 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
96 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
97 end
97 end
98
98
99 def test_project_filter_in_global_queries
99 def test_project_filter_in_global_queries
100 query = IssueQuery.new(:project => nil, :name => '_')
100 query = IssueQuery.new(:project => nil, :name => '_')
101 project_filter = query.available_filters["project_id"]
101 project_filter = query.available_filters["project_id"]
102 assert_not_nil project_filter
102 assert_not_nil project_filter
103 project_ids = project_filter[:values].map{|p| p[1]}
103 project_ids = project_filter[:values].map{|p| p[1]}
104 assert project_ids.include?("1") #public project
104 assert project_ids.include?("1") #public project
105 assert !project_ids.include?("2") #private project user cannot see
105 assert !project_ids.include?("2") #private project user cannot see
106 end
106 end
107
107
108 def test_available_filters_should_not_include_fields_disabled_on_all_trackers
108 def test_available_filters_should_not_include_fields_disabled_on_all_trackers
109 Tracker.all.each do |tracker|
109 Tracker.all.each do |tracker|
110 tracker.core_fields = Tracker::CORE_FIELDS - ['start_date']
110 tracker.core_fields = Tracker::CORE_FIELDS - ['start_date']
111 tracker.save!
111 tracker.save!
112 end
112 end
113
113
114 query = IssueQuery.new(:name => '_')
114 query = IssueQuery.new(:name => '_')
115 assert_include 'due_date', query.available_filters
115 assert_include 'due_date', query.available_filters
116 assert_not_include 'start_date', query.available_filters
116 assert_not_include 'start_date', query.available_filters
117 end
117 end
118
118
119 def find_issues_with_query(query)
119 def find_issues_with_query(query)
120 Issue.joins(:status, :tracker, :project, :priority).where(
120 Issue.joins(:status, :tracker, :project, :priority).where(
121 query.statement
121 query.statement
122 ).to_a
122 ).to_a
123 end
123 end
124
124
125 def assert_find_issues_with_query_is_successful(query)
125 def assert_find_issues_with_query_is_successful(query)
126 assert_nothing_raised do
126 assert_nothing_raised do
127 find_issues_with_query(query)
127 find_issues_with_query(query)
128 end
128 end
129 end
129 end
130
130
131 def assert_query_statement_includes(query, condition)
131 def assert_query_statement_includes(query, condition)
132 assert_include condition, query.statement
132 assert_include condition, query.statement
133 end
133 end
134
134
135 def assert_query_result(expected, query)
135 def assert_query_result(expected, query)
136 assert_nothing_raised do
136 assert_nothing_raised do
137 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
137 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
138 assert_equal expected.size, query.issue_count
138 assert_equal expected.size, query.issue_count
139 end
139 end
140 end
140 end
141
141
142 def test_query_should_allow_shared_versions_for_a_project_query
142 def test_query_should_allow_shared_versions_for_a_project_query
143 subproject_version = Version.find(4)
143 subproject_version = Version.find(4)
144 query = IssueQuery.new(:project => Project.find(1), :name => '_')
144 query = IssueQuery.new(:project => Project.find(1), :name => '_')
145 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
145 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
146
146
147 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
147 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
148 end
148 end
149
149
150 def test_query_with_multiple_custom_fields
150 def test_query_with_multiple_custom_fields
151 query = IssueQuery.find(1)
151 query = IssueQuery.find(1)
152 assert query.valid?
152 assert query.valid?
153 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
153 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
154 issues = find_issues_with_query(query)
154 issues = find_issues_with_query(query)
155 assert_equal 1, issues.length
155 assert_equal 1, issues.length
156 assert_equal Issue.find(3), issues.first
156 assert_equal Issue.find(3), issues.first
157 end
157 end
158
158
159 def test_operator_none
159 def test_operator_none
160 query = IssueQuery.new(:project => Project.find(1), :name => '_')
160 query = IssueQuery.new(:project => Project.find(1), :name => '_')
161 query.add_filter('fixed_version_id', '!*', [''])
161 query.add_filter('fixed_version_id', '!*', [''])
162 query.add_filter('cf_1', '!*', [''])
162 query.add_filter('cf_1', '!*', [''])
163 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
163 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
164 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
164 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
165 find_issues_with_query(query)
165 find_issues_with_query(query)
166 end
166 end
167
167
168 def test_operator_none_for_integer
168 def test_operator_none_for_integer
169 query = IssueQuery.new(:project => Project.find(1), :name => '_')
169 query = IssueQuery.new(:project => Project.find(1), :name => '_')
170 query.add_filter('estimated_hours', '!*', [''])
170 query.add_filter('estimated_hours', '!*', [''])
171 issues = find_issues_with_query(query)
171 issues = find_issues_with_query(query)
172 assert !issues.empty?
172 assert !issues.empty?
173 assert issues.all? {|i| !i.estimated_hours}
173 assert issues.all? {|i| !i.estimated_hours}
174 end
174 end
175
175
176 def test_operator_none_for_date
176 def test_operator_none_for_date
177 query = IssueQuery.new(:project => Project.find(1), :name => '_')
177 query = IssueQuery.new(:project => Project.find(1), :name => '_')
178 query.add_filter('start_date', '!*', [''])
178 query.add_filter('start_date', '!*', [''])
179 issues = find_issues_with_query(query)
179 issues = find_issues_with_query(query)
180 assert !issues.empty?
180 assert !issues.empty?
181 assert issues.all? {|i| i.start_date.nil?}
181 assert issues.all? {|i| i.start_date.nil?}
182 end
182 end
183
183
184 def test_operator_none_for_string_custom_field
184 def test_operator_none_for_string_custom_field
185 query = IssueQuery.new(:project => Project.find(1), :name => '_')
185 query = IssueQuery.new(:project => Project.find(1), :name => '_')
186 query.add_filter('cf_2', '!*', [''])
186 query.add_filter('cf_2', '!*', [''])
187 assert query.has_filter?('cf_2')
187 assert query.has_filter?('cf_2')
188 issues = find_issues_with_query(query)
188 issues = find_issues_with_query(query)
189 assert !issues.empty?
189 assert !issues.empty?
190 assert issues.all? {|i| i.custom_field_value(2).blank?}
190 assert issues.all? {|i| i.custom_field_value(2).blank?}
191 end
191 end
192
192
193 def test_operator_all
193 def test_operator_all
194 query = IssueQuery.new(:project => Project.find(1), :name => '_')
194 query = IssueQuery.new(:project => Project.find(1), :name => '_')
195 query.add_filter('fixed_version_id', '*', [''])
195 query.add_filter('fixed_version_id', '*', [''])
196 query.add_filter('cf_1', '*', [''])
196 query.add_filter('cf_1', '*', [''])
197 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
197 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
198 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
198 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
199 find_issues_with_query(query)
199 find_issues_with_query(query)
200 end
200 end
201
201
202 def test_operator_all_for_date
202 def test_operator_all_for_date
203 query = IssueQuery.new(:project => Project.find(1), :name => '_')
203 query = IssueQuery.new(:project => Project.find(1), :name => '_')
204 query.add_filter('start_date', '*', [''])
204 query.add_filter('start_date', '*', [''])
205 issues = find_issues_with_query(query)
205 issues = find_issues_with_query(query)
206 assert !issues.empty?
206 assert !issues.empty?
207 assert issues.all? {|i| i.start_date.present?}
207 assert issues.all? {|i| i.start_date.present?}
208 end
208 end
209
209
210 def test_operator_all_for_string_custom_field
210 def test_operator_all_for_string_custom_field
211 query = IssueQuery.new(:project => Project.find(1), :name => '_')
211 query = IssueQuery.new(:project => Project.find(1), :name => '_')
212 query.add_filter('cf_2', '*', [''])
212 query.add_filter('cf_2', '*', [''])
213 assert query.has_filter?('cf_2')
213 assert query.has_filter?('cf_2')
214 issues = find_issues_with_query(query)
214 issues = find_issues_with_query(query)
215 assert !issues.empty?
215 assert !issues.empty?
216 assert issues.all? {|i| i.custom_field_value(2).present?}
216 assert issues.all? {|i| i.custom_field_value(2).present?}
217 end
217 end
218
218
219 def test_numeric_filter_should_not_accept_non_numeric_values
219 def test_numeric_filter_should_not_accept_non_numeric_values
220 query = IssueQuery.new(:name => '_')
220 query = IssueQuery.new(:name => '_')
221 query.add_filter('estimated_hours', '=', ['a'])
221 query.add_filter('estimated_hours', '=', ['a'])
222
222
223 assert query.has_filter?('estimated_hours')
223 assert query.has_filter?('estimated_hours')
224 assert !query.valid?
224 assert !query.valid?
225 end
225 end
226
226
227 def test_operator_is_on_float
227 def test_operator_is_on_float
228 Issue.where(:id => 2).update_all("estimated_hours = 171.2")
228 Issue.where(:id => 2).update_all("estimated_hours = 171.2")
229 query = IssueQuery.new(:name => '_')
229 query = IssueQuery.new(:name => '_')
230 query.add_filter('estimated_hours', '=', ['171.20'])
230 query.add_filter('estimated_hours', '=', ['171.20'])
231 issues = find_issues_with_query(query)
231 issues = find_issues_with_query(query)
232 assert_equal 1, issues.size
232 assert_equal 1, issues.size
233 assert_equal 2, issues.first.id
233 assert_equal 2, issues.first.id
234 end
234 end
235
235
236 def test_operator_is_on_integer_custom_field
236 def test_operator_is_on_integer_custom_field
237 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
237 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
238 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
238 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
239 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
239 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
240 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
240 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
241
241
242 query = IssueQuery.new(:name => '_')
242 query = IssueQuery.new(:name => '_')
243 query.add_filter("cf_#{f.id}", '=', ['12'])
243 query.add_filter("cf_#{f.id}", '=', ['12'])
244 issues = find_issues_with_query(query)
244 issues = find_issues_with_query(query)
245 assert_equal 1, issues.size
245 assert_equal 1, issues.size
246 assert_equal 2, issues.first.id
246 assert_equal 2, issues.first.id
247 end
247 end
248
248
249 def test_operator_is_on_integer_custom_field_should_accept_negative_value
249 def test_operator_is_on_integer_custom_field_should_accept_negative_value
250 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
250 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
251 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
251 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
252 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
252 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
253 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
253 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
254
254
255 query = IssueQuery.new(:name => '_')
255 query = IssueQuery.new(:name => '_')
256 query.add_filter("cf_#{f.id}", '=', ['-12'])
256 query.add_filter("cf_#{f.id}", '=', ['-12'])
257 assert query.valid?
257 assert query.valid?
258 issues = find_issues_with_query(query)
258 issues = find_issues_with_query(query)
259 assert_equal 1, issues.size
259 assert_equal 1, issues.size
260 assert_equal 2, issues.first.id
260 assert_equal 2, issues.first.id
261 end
261 end
262
262
263 def test_operator_is_on_float_custom_field
263 def test_operator_is_on_float_custom_field
264 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
264 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
265 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
265 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
266 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
266 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
267 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
267 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
268
268
269 query = IssueQuery.new(:name => '_')
269 query = IssueQuery.new(:name => '_')
270 query.add_filter("cf_#{f.id}", '=', ['12.7'])
270 query.add_filter("cf_#{f.id}", '=', ['12.7'])
271 issues = find_issues_with_query(query)
271 issues = find_issues_with_query(query)
272 assert_equal 1, issues.size
272 assert_equal 1, issues.size
273 assert_equal 2, issues.first.id
273 assert_equal 2, issues.first.id
274 end
274 end
275
275
276 def test_operator_is_on_float_custom_field_should_accept_negative_value
276 def test_operator_is_on_float_custom_field_should_accept_negative_value
277 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
277 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
278 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
278 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
279 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
279 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
280 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
280 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
281
281
282 query = IssueQuery.new(:name => '_')
282 query = IssueQuery.new(:name => '_')
283 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
283 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
284 assert query.valid?
284 assert query.valid?
285 issues = find_issues_with_query(query)
285 issues = find_issues_with_query(query)
286 assert_equal 1, issues.size
286 assert_equal 1, issues.size
287 assert_equal 2, issues.first.id
287 assert_equal 2, issues.first.id
288 end
288 end
289
289
290 def test_operator_is_on_multi_list_custom_field
290 def test_operator_is_on_multi_list_custom_field
291 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
291 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
292 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
292 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
293 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
293 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
294 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
294 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
295 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
295 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
296
296
297 query = IssueQuery.new(:name => '_')
297 query = IssueQuery.new(:name => '_')
298 query.add_filter("cf_#{f.id}", '=', ['value1'])
298 query.add_filter("cf_#{f.id}", '=', ['value1'])
299 issues = find_issues_with_query(query)
299 issues = find_issues_with_query(query)
300 assert_equal [1, 3], issues.map(&:id).sort
300 assert_equal [1, 3], issues.map(&:id).sort
301
301
302 query = IssueQuery.new(:name => '_')
302 query = IssueQuery.new(:name => '_')
303 query.add_filter("cf_#{f.id}", '=', ['value2'])
303 query.add_filter("cf_#{f.id}", '=', ['value2'])
304 issues = find_issues_with_query(query)
304 issues = find_issues_with_query(query)
305 assert_equal [1], issues.map(&:id).sort
305 assert_equal [1], issues.map(&:id).sort
306 end
306 end
307
307
308 def test_operator_is_not_on_multi_list_custom_field
308 def test_operator_is_not_on_multi_list_custom_field
309 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
309 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
310 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
310 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
311 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
311 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
312 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
312 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
313 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
313 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
314
314
315 query = IssueQuery.new(:name => '_')
315 query = IssueQuery.new(:name => '_')
316 query.add_filter("cf_#{f.id}", '!', ['value1'])
316 query.add_filter("cf_#{f.id}", '!', ['value1'])
317 issues = find_issues_with_query(query)
317 issues = find_issues_with_query(query)
318 assert !issues.map(&:id).include?(1)
318 assert !issues.map(&:id).include?(1)
319 assert !issues.map(&:id).include?(3)
319 assert !issues.map(&:id).include?(3)
320
320
321 query = IssueQuery.new(:name => '_')
321 query = IssueQuery.new(:name => '_')
322 query.add_filter("cf_#{f.id}", '!', ['value2'])
322 query.add_filter("cf_#{f.id}", '!', ['value2'])
323 issues = find_issues_with_query(query)
323 issues = find_issues_with_query(query)
324 assert !issues.map(&:id).include?(1)
324 assert !issues.map(&:id).include?(1)
325 assert issues.map(&:id).include?(3)
325 assert issues.map(&:id).include?(3)
326 end
326 end
327
327
328 def test_operator_is_on_is_private_field
328 def test_operator_is_on_is_private_field
329 # is_private filter only available for those who can set issues private
329 # is_private filter only available for those who can set issues private
330 User.current = User.find(2)
330 User.current = User.find(2)
331
331
332 query = IssueQuery.new(:name => '_')
332 query = IssueQuery.new(:name => '_')
333 assert query.available_filters.key?('is_private')
333 assert query.available_filters.key?('is_private')
334
334
335 query.add_filter("is_private", '=', ['1'])
335 query.add_filter("is_private", '=', ['1'])
336 issues = find_issues_with_query(query)
336 issues = find_issues_with_query(query)
337 assert issues.any?
337 assert issues.any?
338 assert_nil issues.detect {|issue| !issue.is_private?}
338 assert_nil issues.detect {|issue| !issue.is_private?}
339 ensure
339 ensure
340 User.current = nil
340 User.current = nil
341 end
341 end
342
342
343 def test_operator_is_not_on_is_private_field
343 def test_operator_is_not_on_is_private_field
344 # is_private filter only available for those who can set issues private
344 # is_private filter only available for those who can set issues private
345 User.current = User.find(2)
345 User.current = User.find(2)
346
346
347 query = IssueQuery.new(:name => '_')
347 query = IssueQuery.new(:name => '_')
348 assert query.available_filters.key?('is_private')
348 assert query.available_filters.key?('is_private')
349
349
350 query.add_filter("is_private", '!', ['1'])
350 query.add_filter("is_private", '!', ['1'])
351 issues = find_issues_with_query(query)
351 issues = find_issues_with_query(query)
352 assert issues.any?
352 assert issues.any?
353 assert_nil issues.detect {|issue| issue.is_private?}
353 assert_nil issues.detect {|issue| issue.is_private?}
354 ensure
354 ensure
355 User.current = nil
355 User.current = nil
356 end
356 end
357
357
358 def test_operator_greater_than
358 def test_operator_greater_than
359 query = IssueQuery.new(:project => Project.find(1), :name => '_')
359 query = IssueQuery.new(:project => Project.find(1), :name => '_')
360 query.add_filter('done_ratio', '>=', ['40'])
360 query.add_filter('done_ratio', '>=', ['40'])
361 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
361 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
362 find_issues_with_query(query)
362 find_issues_with_query(query)
363 end
363 end
364
364
365 def test_operator_greater_than_a_float
365 def test_operator_greater_than_a_float
366 query = IssueQuery.new(:project => Project.find(1), :name => '_')
366 query = IssueQuery.new(:project => Project.find(1), :name => '_')
367 query.add_filter('estimated_hours', '>=', ['40.5'])
367 query.add_filter('estimated_hours', '>=', ['40.5'])
368 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
368 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
369 find_issues_with_query(query)
369 find_issues_with_query(query)
370 end
370 end
371
371
372 def test_operator_greater_than_on_int_custom_field
372 def test_operator_greater_than_on_int_custom_field
373 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
373 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
374 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
374 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
375 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
375 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
376 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
376 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
377
377
378 query = IssueQuery.new(:project => Project.find(1), :name => '_')
378 query = IssueQuery.new(:project => Project.find(1), :name => '_')
379 query.add_filter("cf_#{f.id}", '>=', ['8'])
379 query.add_filter("cf_#{f.id}", '>=', ['8'])
380 issues = find_issues_with_query(query)
380 issues = find_issues_with_query(query)
381 assert_equal 1, issues.size
381 assert_equal 1, issues.size
382 assert_equal 2, issues.first.id
382 assert_equal 2, issues.first.id
383 end
383 end
384
384
385 def test_operator_lesser_than
385 def test_operator_lesser_than
386 query = IssueQuery.new(:project => Project.find(1), :name => '_')
386 query = IssueQuery.new(:project => Project.find(1), :name => '_')
387 query.add_filter('done_ratio', '<=', ['30'])
387 query.add_filter('done_ratio', '<=', ['30'])
388 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
388 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
389 find_issues_with_query(query)
389 find_issues_with_query(query)
390 end
390 end
391
391
392 def test_operator_lesser_than_on_custom_field
392 def test_operator_lesser_than_on_custom_field
393 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
393 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
394 query = IssueQuery.new(:project => Project.find(1), :name => '_')
394 query = IssueQuery.new(:project => Project.find(1), :name => '_')
395 query.add_filter("cf_#{f.id}", '<=', ['30'])
395 query.add_filter("cf_#{f.id}", '<=', ['30'])
396 assert_match /CAST.+ <= 30\.0/, query.statement
396 assert_match /CAST.+ <= 30\.0/, query.statement
397 find_issues_with_query(query)
397 find_issues_with_query(query)
398 end
398 end
399
399
400 def test_operator_lesser_than_on_date_custom_field
400 def test_operator_lesser_than_on_date_custom_field
401 f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
401 f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
402 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11')
402 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11')
403 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14')
403 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14')
404 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
404 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
405
405
406 query = IssueQuery.new(:project => Project.find(1), :name => '_')
406 query = IssueQuery.new(:project => Project.find(1), :name => '_')
407 query.add_filter("cf_#{f.id}", '<=', ['2013-05-01'])
407 query.add_filter("cf_#{f.id}", '<=', ['2013-05-01'])
408 issue_ids = find_issues_with_query(query).map(&:id)
408 issue_ids = find_issues_with_query(query).map(&:id)
409 assert_include 1, issue_ids
409 assert_include 1, issue_ids
410 assert_not_include 2, issue_ids
410 assert_not_include 2, issue_ids
411 assert_not_include 3, issue_ids
411 assert_not_include 3, issue_ids
412 end
412 end
413
413
414 def test_operator_between
414 def test_operator_between
415 query = IssueQuery.new(:project => Project.find(1), :name => '_')
415 query = IssueQuery.new(:project => Project.find(1), :name => '_')
416 query.add_filter('done_ratio', '><', ['30', '40'])
416 query.add_filter('done_ratio', '><', ['30', '40'])
417 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
417 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
418 find_issues_with_query(query)
418 find_issues_with_query(query)
419 end
419 end
420
420
421 def test_operator_between_on_custom_field
421 def test_operator_between_on_custom_field
422 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
422 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
423 query = IssueQuery.new(:project => Project.find(1), :name => '_')
423 query = IssueQuery.new(:project => Project.find(1), :name => '_')
424 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
424 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
425 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
425 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
426 find_issues_with_query(query)
426 find_issues_with_query(query)
427 end
427 end
428
428
429 def test_date_filter_should_not_accept_non_date_values
429 def test_date_filter_should_not_accept_non_date_values
430 query = IssueQuery.new(:name => '_')
430 query = IssueQuery.new(:name => '_')
431 query.add_filter('created_on', '=', ['a'])
431 query.add_filter('created_on', '=', ['a'])
432
432
433 assert query.has_filter?('created_on')
433 assert query.has_filter?('created_on')
434 assert !query.valid?
434 assert !query.valid?
435 end
435 end
436
436
437 def test_date_filter_should_not_accept_invalid_date_values
437 def test_date_filter_should_not_accept_invalid_date_values
438 query = IssueQuery.new(:name => '_')
438 query = IssueQuery.new(:name => '_')
439 query.add_filter('created_on', '=', ['2011-01-34'])
439 query.add_filter('created_on', '=', ['2011-01-34'])
440
440
441 assert query.has_filter?('created_on')
441 assert query.has_filter?('created_on')
442 assert !query.valid?
442 assert !query.valid?
443 end
443 end
444
444
445 def test_relative_date_filter_should_not_accept_non_integer_values
445 def test_relative_date_filter_should_not_accept_non_integer_values
446 query = IssueQuery.new(:name => '_')
446 query = IssueQuery.new(:name => '_')
447 query.add_filter('created_on', '>t-', ['a'])
447 query.add_filter('created_on', '>t-', ['a'])
448
448
449 assert query.has_filter?('created_on')
449 assert query.has_filter?('created_on')
450 assert !query.valid?
450 assert !query.valid?
451 end
451 end
452
452
453 def test_operator_date_equals
453 def test_operator_date_equals
454 query = IssueQuery.new(:name => '_')
454 query = IssueQuery.new(:name => '_')
455 query.add_filter('due_date', '=', ['2011-07-10'])
455 query.add_filter('due_date', '=', ['2011-07-10'])
456 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/,
456 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/,
457 query.statement
457 query.statement
458 find_issues_with_query(query)
458 find_issues_with_query(query)
459 end
459 end
460
460
461 def test_operator_date_lesser_than
461 def test_operator_date_lesser_than
462 query = IssueQuery.new(:name => '_')
462 query = IssueQuery.new(:name => '_')
463 query.add_filter('due_date', '<=', ['2011-07-10'])
463 query.add_filter('due_date', '<=', ['2011-07-10'])
464 assert_match /issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/, query.statement
464 assert_match /issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/, query.statement
465 find_issues_with_query(query)
465 find_issues_with_query(query)
466 end
466 end
467
467
468 def test_operator_date_lesser_than_with_timestamp
468 def test_operator_date_lesser_than_with_timestamp
469 query = IssueQuery.new(:name => '_')
469 query = IssueQuery.new(:name => '_')
470 query.add_filter('updated_on', '<=', ['2011-07-10T19:13:52'])
470 query.add_filter('updated_on', '<=', ['2011-07-10T19:13:52'])
471 assert_match /issues\.updated_on <= '#{quoted_date "2011-07-10"} 19:13:52/, query.statement
471 assert_match /issues\.updated_on <= '#{quoted_date "2011-07-10"} 19:13:52/, query.statement
472 find_issues_with_query(query)
472 find_issues_with_query(query)
473 end
473 end
474
474
475 def test_operator_date_greater_than
475 def test_operator_date_greater_than
476 query = IssueQuery.new(:name => '_')
476 query = IssueQuery.new(:name => '_')
477 query.add_filter('due_date', '>=', ['2011-07-10'])
477 query.add_filter('due_date', '>=', ['2011-07-10'])
478 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?'/, query.statement
478 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?'/, query.statement
479 find_issues_with_query(query)
479 find_issues_with_query(query)
480 end
480 end
481
481
482 def test_operator_date_greater_than_with_timestamp
482 def test_operator_date_greater_than_with_timestamp
483 query = IssueQuery.new(:name => '_')
483 query = IssueQuery.new(:name => '_')
484 query.add_filter('updated_on', '>=', ['2011-07-10T19:13:52'])
484 query.add_filter('updated_on', '>=', ['2011-07-10T19:13:52'])
485 assert_match /issues\.updated_on > '#{quoted_date "2011-07-10"} 19:13:51(\.0+)?'/, query.statement
485 assert_match /issues\.updated_on > '#{quoted_date "2011-07-10"} 19:13:51(\.0+)?'/, query.statement
486 find_issues_with_query(query)
486 find_issues_with_query(query)
487 end
487 end
488
488
489 def test_operator_date_between
489 def test_operator_date_between
490 query = IssueQuery.new(:name => '_')
490 query = IssueQuery.new(:name => '_')
491 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
491 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
492 assert_match /issues\.due_date > '#{quoted_date "2011-06-22"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?'/,
492 assert_match /issues\.due_date > '#{quoted_date "2011-06-22"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?'/,
493 query.statement
493 query.statement
494 find_issues_with_query(query)
494 find_issues_with_query(query)
495 end
495 end
496
496
497 def test_operator_in_more_than
497 def test_operator_in_more_than
498 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
498 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
499 query = IssueQuery.new(:project => Project.find(1), :name => '_')
499 query = IssueQuery.new(:project => Project.find(1), :name => '_')
500 query.add_filter('due_date', '>t+', ['15'])
500 query.add_filter('due_date', '>t+', ['15'])
501 issues = find_issues_with_query(query)
501 issues = find_issues_with_query(query)
502 assert !issues.empty?
502 assert !issues.empty?
503 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
503 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
504 end
504 end
505
505
506 def test_operator_in_less_than
506 def test_operator_in_less_than
507 query = IssueQuery.new(:project => Project.find(1), :name => '_')
507 query = IssueQuery.new(:project => Project.find(1), :name => '_')
508 query.add_filter('due_date', '<t+', ['15'])
508 query.add_filter('due_date', '<t+', ['15'])
509 issues = find_issues_with_query(query)
509 issues = find_issues_with_query(query)
510 assert !issues.empty?
510 assert !issues.empty?
511 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
511 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
512 end
512 end
513
513
514 def test_operator_in_the_next_days
514 def test_operator_in_the_next_days
515 query = IssueQuery.new(:project => Project.find(1), :name => '_')
515 query = IssueQuery.new(:project => Project.find(1), :name => '_')
516 query.add_filter('due_date', '><t+', ['15'])
516 query.add_filter('due_date', '><t+', ['15'])
517 issues = find_issues_with_query(query)
517 issues = find_issues_with_query(query)
518 assert !issues.empty?
518 assert !issues.empty?
519 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
519 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
520 end
520 end
521
521
522 def test_operator_less_than_ago
522 def test_operator_less_than_ago
523 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
523 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
524 query = IssueQuery.new(:project => Project.find(1), :name => '_')
524 query = IssueQuery.new(:project => Project.find(1), :name => '_')
525 query.add_filter('due_date', '>t-', ['3'])
525 query.add_filter('due_date', '>t-', ['3'])
526 issues = find_issues_with_query(query)
526 issues = find_issues_with_query(query)
527 assert !issues.empty?
527 assert !issues.empty?
528 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
528 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
529 end
529 end
530
530
531 def test_operator_in_the_past_days
531 def test_operator_in_the_past_days
532 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
532 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
533 query = IssueQuery.new(:project => Project.find(1), :name => '_')
533 query = IssueQuery.new(:project => Project.find(1), :name => '_')
534 query.add_filter('due_date', '><t-', ['3'])
534 query.add_filter('due_date', '><t-', ['3'])
535 issues = find_issues_with_query(query)
535 issues = find_issues_with_query(query)
536 assert !issues.empty?
536 assert !issues.empty?
537 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
537 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
538 end
538 end
539
539
540 def test_operator_more_than_ago
540 def test_operator_more_than_ago
541 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
541 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
542 query = IssueQuery.new(:project => Project.find(1), :name => '_')
542 query = IssueQuery.new(:project => Project.find(1), :name => '_')
543 query.add_filter('due_date', '<t-', ['10'])
543 query.add_filter('due_date', '<t-', ['10'])
544 assert query.statement.include?("#{Issue.table_name}.due_date <=")
544 assert query.statement.include?("#{Issue.table_name}.due_date <=")
545 issues = find_issues_with_query(query)
545 issues = find_issues_with_query(query)
546 assert !issues.empty?
546 assert !issues.empty?
547 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
547 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
548 end
548 end
549
549
550 def test_operator_in
550 def test_operator_in
551 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
551 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
552 query = IssueQuery.new(:project => Project.find(1), :name => '_')
552 query = IssueQuery.new(:project => Project.find(1), :name => '_')
553 query.add_filter('due_date', 't+', ['2'])
553 query.add_filter('due_date', 't+', ['2'])
554 issues = find_issues_with_query(query)
554 issues = find_issues_with_query(query)
555 assert !issues.empty?
555 assert !issues.empty?
556 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
556 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
557 end
557 end
558
558
559 def test_operator_ago
559 def test_operator_ago
560 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
560 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
561 query = IssueQuery.new(:project => Project.find(1), :name => '_')
561 query = IssueQuery.new(:project => Project.find(1), :name => '_')
562 query.add_filter('due_date', 't-', ['3'])
562 query.add_filter('due_date', 't-', ['3'])
563 issues = find_issues_with_query(query)
563 issues = find_issues_with_query(query)
564 assert !issues.empty?
564 assert !issues.empty?
565 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
565 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
566 end
566 end
567
567
568 def test_operator_today
568 def test_operator_today
569 query = IssueQuery.new(:project => Project.find(1), :name => '_')
569 query = IssueQuery.new(:project => Project.find(1), :name => '_')
570 query.add_filter('due_date', 't', [''])
570 query.add_filter('due_date', 't', [''])
571 issues = find_issues_with_query(query)
571 issues = find_issues_with_query(query)
572 assert !issues.empty?
572 assert !issues.empty?
573 issues.each {|issue| assert_equal Date.today, issue.due_date}
573 issues.each {|issue| assert_equal Date.today, issue.due_date}
574 end
574 end
575
575
576 def test_operator_date_periods
576 def test_operator_date_periods
577 %w(t ld w lw l2w m lm y).each do |operator|
577 %w(t ld w lw l2w m lm y).each do |operator|
578 query = IssueQuery.new(:name => '_')
578 query = IssueQuery.new(:name => '_')
579 query.add_filter('due_date', operator, [''])
579 query.add_filter('due_date', operator, [''])
580 assert query.valid?
580 assert query.valid?
581 assert query.issues
581 assert query.issues
582 end
582 end
583 end
583 end
584
584
585 def test_operator_datetime_periods
585 def test_operator_datetime_periods
586 %w(t ld w lw l2w m lm y).each do |operator|
586 %w(t ld w lw l2w m lm y).each do |operator|
587 query = IssueQuery.new(:name => '_')
587 query = IssueQuery.new(:name => '_')
588 query.add_filter('created_on', operator, [''])
588 query.add_filter('created_on', operator, [''])
589 assert query.valid?
589 assert query.valid?
590 assert query.issues
590 assert query.issues
591 end
591 end
592 end
592 end
593
593
594 def test_operator_contains
594 def test_operator_contains
595 issue = Issue.generate!(:subject => 'AbCdEfG')
595 issue = Issue.generate!(:subject => 'AbCdEfG')
596
596
597 query = IssueQuery.new(:name => '_')
597 query = IssueQuery.new(:name => '_')
598 query.add_filter('subject', '~', ['cdeF'])
598 query.add_filter('subject', '~', ['cdeF'])
599 result = find_issues_with_query(query)
599 result = find_issues_with_query(query)
600 assert_include issue, result
600 assert_include issue, result
601 result.each {|issue| assert issue.subject.downcase.include?('cdef') }
601 result.each {|issue| assert issue.subject.downcase.include?('cdef') }
602 end
602 end
603
603
604 def test_operator_does_not_contain
604 def test_operator_does_not_contain
605 issue = Issue.generate!(:subject => 'AbCdEfG')
605 issue = Issue.generate!(:subject => 'AbCdEfG')
606
606
607 query = IssueQuery.new(:name => '_')
607 query = IssueQuery.new(:name => '_')
608 query.add_filter('subject', '!~', ['cdeF'])
608 query.add_filter('subject', '!~', ['cdeF'])
609 result = find_issues_with_query(query)
609 result = find_issues_with_query(query)
610 assert_not_include issue, result
610 assert_not_include issue, result
611 end
611 end
612
612
613 def test_range_for_this_week_with_week_starting_on_monday
613 def test_range_for_this_week_with_week_starting_on_monday
614 I18n.locale = :fr
614 I18n.locale = :fr
615 assert_equal '1', I18n.t(:general_first_day_of_week)
615 assert_equal '1', I18n.t(:general_first_day_of_week)
616
616
617 Date.stubs(:today).returns(Date.parse('2011-04-29'))
617 Date.stubs(:today).returns(Date.parse('2011-04-29'))
618
618
619 query = IssueQuery.new(:project => Project.find(1), :name => '_')
619 query = IssueQuery.new(:project => Project.find(1), :name => '_')
620 query.add_filter('due_date', 'w', [''])
620 query.add_filter('due_date', 'w', [''])
621 assert_match /issues\.due_date > '#{quoted_date "2011-04-24"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-05-01"} 23:59:59(\.\d+)?/,
621 assert_match /issues\.due_date > '#{quoted_date "2011-04-24"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-05-01"} 23:59:59(\.\d+)?/,
622 query.statement
622 query.statement
623 I18n.locale = :en
623 I18n.locale = :en
624 end
624 end
625
625
626 def test_range_for_this_week_with_week_starting_on_sunday
626 def test_range_for_this_week_with_week_starting_on_sunday
627 I18n.locale = :en
627 I18n.locale = :en
628 assert_equal '7', I18n.t(:general_first_day_of_week)
628 assert_equal '7', I18n.t(:general_first_day_of_week)
629
629
630 Date.stubs(:today).returns(Date.parse('2011-04-29'))
630 Date.stubs(:today).returns(Date.parse('2011-04-29'))
631
631
632 query = IssueQuery.new(:project => Project.find(1), :name => '_')
632 query = IssueQuery.new(:project => Project.find(1), :name => '_')
633 query.add_filter('due_date', 'w', [''])
633 query.add_filter('due_date', 'w', [''])
634 assert_match /issues\.due_date > '#{quoted_date "2011-04-23"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-04-30"} 23:59:59(\.\d+)?/,
634 assert_match /issues\.due_date > '#{quoted_date "2011-04-23"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-04-30"} 23:59:59(\.\d+)?/,
635 query.statement
635 query.statement
636 end
636 end
637
637
638 def test_filter_assigned_to_me
638 def test_filter_assigned_to_me
639 user = User.find(2)
639 user = User.find(2)
640 group = Group.find(10)
640 group = Group.find(10)
641 User.current = user
641 User.current = user
642 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
642 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
643 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
643 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
644 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
644 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
645 group.users << user
645 group.users << user
646
646
647 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
647 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
648 result = query.issues
648 result = query.issues
649 assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
649 assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
650
650
651 assert result.include?(i1)
651 assert result.include?(i1)
652 assert result.include?(i2)
652 assert result.include?(i2)
653 assert !result.include?(i3)
653 assert !result.include?(i3)
654 end
654 end
655
655
656 def test_user_custom_field_filtered_on_me
656 def test_user_custom_field_filtered_on_me
657 User.current = User.find(2)
657 User.current = User.find(2)
658 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
658 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
659 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
659 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
660 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
660 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
661
661
662 query = IssueQuery.new(:name => '_', :project => Project.find(1))
662 query = IssueQuery.new(:name => '_', :project => Project.find(1))
663 filter = query.available_filters["cf_#{cf.id}"]
663 filter = query.available_filters["cf_#{cf.id}"]
664 assert_not_nil filter
664 assert_not_nil filter
665 assert_include 'me', filter[:values].map{|v| v[1]}
665 assert_include 'me', filter[:values].map{|v| v[1]}
666
666
667 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
667 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
668 result = query.issues
668 result = query.issues
669 assert_equal 1, result.size
669 assert_equal 1, result.size
670 assert_equal issue1, result.first
670 assert_equal issue1, result.first
671 end
671 end
672
672
673 def test_filter_on_me_by_anonymous_user
673 def test_filter_on_me_by_anonymous_user
674 User.current = nil
674 User.current = nil
675 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
675 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
676 assert_equal [], query.issues
676 assert_equal [], query.issues
677 end
677 end
678
678
679 def test_filter_my_projects
679 def test_filter_my_projects
680 User.current = User.find(2)
680 User.current = User.find(2)
681 query = IssueQuery.new(:name => '_')
681 query = IssueQuery.new(:name => '_')
682 filter = query.available_filters['project_id']
682 filter = query.available_filters['project_id']
683 assert_not_nil filter
683 assert_not_nil filter
684 assert_include 'mine', filter[:values].map{|v| v[1]}
684 assert_include 'mine', filter[:values].map{|v| v[1]}
685
685
686 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
686 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
687 result = query.issues
687 result = query.issues
688 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
688 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
689 end
689 end
690
690
691 def test_filter_watched_issues
691 def test_filter_watched_issues
692 User.current = User.find(1)
692 User.current = User.find(1)
693 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
693 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
694 result = find_issues_with_query(query)
694 result = find_issues_with_query(query)
695 assert_not_nil result
695 assert_not_nil result
696 assert !result.empty?
696 assert !result.empty?
697 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
697 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
698 User.current = nil
698 User.current = nil
699 end
699 end
700
700
701 def test_filter_unwatched_issues
701 def test_filter_unwatched_issues
702 User.current = User.find(1)
702 User.current = User.find(1)
703 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
703 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
704 result = find_issues_with_query(query)
704 result = find_issues_with_query(query)
705 assert_not_nil result
705 assert_not_nil result
706 assert !result.empty?
706 assert !result.empty?
707 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
707 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
708 User.current = nil
708 User.current = nil
709 end
709 end
710
710
711 def test_filter_on_custom_field_should_ignore_projects_with_field_disabled
711 def test_filter_on_custom_field_should_ignore_projects_with_field_disabled
712 field = IssueCustomField.generate!(:trackers => Tracker.all, :project_ids => [1, 3, 4], :is_filter => true)
712 field = IssueCustomField.generate!(:trackers => Tracker.all, :project_ids => [1, 3, 4], :is_filter => true)
713 Issue.generate!(:project_id => 3, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
713 Issue.generate!(:project_id => 3, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
714 Issue.generate!(:project_id => 4, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
714 Issue.generate!(:project_id => 4, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
715
715
716 query = IssueQuery.new(:name => '_', :project => Project.find(1))
716 query = IssueQuery.new(:name => '_', :project => Project.find(1))
717 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
717 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
718 assert_equal 2, find_issues_with_query(query).size
718 assert_equal 2, find_issues_with_query(query).size
719
719
720 field.project_ids = [1, 3] # Disable the field for project 4
720 field.project_ids = [1, 3] # Disable the field for project 4
721 field.save!
721 field.save!
722 assert_equal 1, find_issues_with_query(query).size
722 assert_equal 1, find_issues_with_query(query).size
723 end
723 end
724
724
725 def test_filter_on_custom_field_should_ignore_trackers_with_field_disabled
725 def test_filter_on_custom_field_should_ignore_trackers_with_field_disabled
726 field = IssueCustomField.generate!(:tracker_ids => [1, 2], :is_for_all => true, :is_filter => true)
726 field = IssueCustomField.generate!(:tracker_ids => [1, 2], :is_for_all => true, :is_filter => true)
727 Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => 'Foo'})
727 Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => 'Foo'})
728 Issue.generate!(:project_id => 1, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
728 Issue.generate!(:project_id => 1, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
729
729
730 query = IssueQuery.new(:name => '_', :project => Project.find(1))
730 query = IssueQuery.new(:name => '_', :project => Project.find(1))
731 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
731 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
732 assert_equal 2, find_issues_with_query(query).size
732 assert_equal 2, find_issues_with_query(query).size
733
733
734 field.tracker_ids = [1] # Disable the field for tracker 2
734 field.tracker_ids = [1] # Disable the field for tracker 2
735 field.save!
735 field.save!
736 assert_equal 1, find_issues_with_query(query).size
736 assert_equal 1, find_issues_with_query(query).size
737 end
737 end
738
738
739 def test_filter_on_project_custom_field
739 def test_filter_on_project_custom_field
740 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
740 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
741 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
741 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
742 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
742 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
743
743
744 query = IssueQuery.new(:name => '_')
744 query = IssueQuery.new(:name => '_')
745 filter_name = "project.cf_#{field.id}"
745 filter_name = "project.cf_#{field.id}"
746 assert_include filter_name, query.available_filters.keys
746 assert_include filter_name, query.available_filters.keys
747 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
747 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
748 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
748 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
749 end
749 end
750
750
751 def test_filter_on_author_custom_field
751 def test_filter_on_author_custom_field
752 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
752 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
753 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
753 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
754
754
755 query = IssueQuery.new(:name => '_')
755 query = IssueQuery.new(:name => '_')
756 filter_name = "author.cf_#{field.id}"
756 filter_name = "author.cf_#{field.id}"
757 assert_include filter_name, query.available_filters.keys
757 assert_include filter_name, query.available_filters.keys
758 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
758 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
759 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
759 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
760 end
760 end
761
761
762 def test_filter_on_assigned_to_custom_field
762 def test_filter_on_assigned_to_custom_field
763 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
763 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
764 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
764 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
765
765
766 query = IssueQuery.new(:name => '_')
766 query = IssueQuery.new(:name => '_')
767 filter_name = "assigned_to.cf_#{field.id}"
767 filter_name = "assigned_to.cf_#{field.id}"
768 assert_include filter_name, query.available_filters.keys
768 assert_include filter_name, query.available_filters.keys
769 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
769 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
770 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
770 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
771 end
771 end
772
772
773 def test_filter_on_fixed_version_custom_field
773 def test_filter_on_fixed_version_custom_field
774 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
774 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
775 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
775 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
776
776
777 query = IssueQuery.new(:name => '_')
777 query = IssueQuery.new(:name => '_')
778 filter_name = "fixed_version.cf_#{field.id}"
778 filter_name = "fixed_version.cf_#{field.id}"
779 assert_include filter_name, query.available_filters.keys
779 assert_include filter_name, query.available_filters.keys
780 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
780 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
781 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
781 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
782 end
782 end
783
783
784 def test_filter_on_relations_with_a_specific_issue
784 def test_filter_on_relations_with_a_specific_issue
785 IssueRelation.delete_all
785 IssueRelation.delete_all
786 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
786 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
787 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
787 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
788
788
789 query = IssueQuery.new(:name => '_')
789 query = IssueQuery.new(:name => '_')
790 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
790 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
791 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
791 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
792
792
793 query = IssueQuery.new(:name => '_')
793 query = IssueQuery.new(:name => '_')
794 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
794 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
795 assert_equal [1], find_issues_with_query(query).map(&:id).sort
795 assert_equal [1], find_issues_with_query(query).map(&:id).sort
796 end
796 end
797
797
798 def test_filter_on_relations_with_any_issues_in_a_project
798 def test_filter_on_relations_with_any_issues_in_a_project
799 IssueRelation.delete_all
799 IssueRelation.delete_all
800 with_settings :cross_project_issue_relations => '1' do
800 with_settings :cross_project_issue_relations => '1' do
801 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
801 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
802 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
802 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
803 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
803 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
804 end
804 end
805
805
806 query = IssueQuery.new(:name => '_')
806 query = IssueQuery.new(:name => '_')
807 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
807 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
808 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
808 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
809
809
810 query = IssueQuery.new(:name => '_')
810 query = IssueQuery.new(:name => '_')
811 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
811 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
812 assert_equal [1], find_issues_with_query(query).map(&:id).sort
812 assert_equal [1], find_issues_with_query(query).map(&:id).sort
813
813
814 query = IssueQuery.new(:name => '_')
814 query = IssueQuery.new(:name => '_')
815 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
815 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
816 assert_equal [], find_issues_with_query(query).map(&:id).sort
816 assert_equal [], find_issues_with_query(query).map(&:id).sort
817 end
817 end
818
818
819 def test_filter_on_relations_with_any_issues_not_in_a_project
819 def test_filter_on_relations_with_any_issues_not_in_a_project
820 IssueRelation.delete_all
820 IssueRelation.delete_all
821 with_settings :cross_project_issue_relations => '1' do
821 with_settings :cross_project_issue_relations => '1' do
822 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
822 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
823 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
823 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
824 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
824 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
825 end
825 end
826
826
827 query = IssueQuery.new(:name => '_')
827 query = IssueQuery.new(:name => '_')
828 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
828 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
829 assert_equal [1], find_issues_with_query(query).map(&:id).sort
829 assert_equal [1], find_issues_with_query(query).map(&:id).sort
830 end
830 end
831
831
832 def test_filter_on_relations_with_no_issues_in_a_project
832 def test_filter_on_relations_with_no_issues_in_a_project
833 IssueRelation.delete_all
833 IssueRelation.delete_all
834 with_settings :cross_project_issue_relations => '1' do
834 with_settings :cross_project_issue_relations => '1' do
835 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
835 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
836 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
836 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
837 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
837 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
838 end
838 end
839
839
840 query = IssueQuery.new(:name => '_')
840 query = IssueQuery.new(:name => '_')
841 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
841 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
842 ids = find_issues_with_query(query).map(&:id).sort
842 ids = find_issues_with_query(query).map(&:id).sort
843 assert_include 2, ids
843 assert_include 2, ids
844 assert_not_include 1, ids
844 assert_not_include 1, ids
845 assert_not_include 3, ids
845 assert_not_include 3, ids
846 end
846 end
847
847
848 def test_filter_on_relations_with_no_issues
848 def test_filter_on_relations_with_no_issues
849 IssueRelation.delete_all
849 IssueRelation.delete_all
850 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
850 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
851 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
851 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
852
852
853 query = IssueQuery.new(:name => '_')
853 query = IssueQuery.new(:name => '_')
854 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
854 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
855 ids = find_issues_with_query(query).map(&:id)
855 ids = find_issues_with_query(query).map(&:id)
856 assert_equal [], ids & [1, 2, 3]
856 assert_equal [], ids & [1, 2, 3]
857 assert_include 4, ids
857 assert_include 4, ids
858 end
858 end
859
859
860 def test_filter_on_relations_with_any_issues
860 def test_filter_on_relations_with_any_issues
861 IssueRelation.delete_all
861 IssueRelation.delete_all
862 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
862 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
863 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
863 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
864
864
865 query = IssueQuery.new(:name => '_')
865 query = IssueQuery.new(:name => '_')
866 query.filters = {"relates" => {:operator => '*', :values => ['']}}
866 query.filters = {"relates" => {:operator => '*', :values => ['']}}
867 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
867 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
868 end
868 end
869
869
870 def test_filter_on_relations_should_not_ignore_other_filter
870 def test_filter_on_relations_should_not_ignore_other_filter
871 issue = Issue.generate!
871 issue = Issue.generate!
872 issue1 = Issue.generate!(:status_id => 1)
872 issue1 = Issue.generate!(:status_id => 1)
873 issue2 = Issue.generate!(:status_id => 2)
873 issue2 = Issue.generate!(:status_id => 2)
874 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue1)
874 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue1)
875 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue2)
875 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue2)
876
876
877 query = IssueQuery.new(:name => '_')
877 query = IssueQuery.new(:name => '_')
878 query.filters = {
878 query.filters = {
879 "status_id" => {:operator => '=', :values => ['1']},
879 "status_id" => {:operator => '=', :values => ['1']},
880 "relates" => {:operator => '=', :values => [issue.id.to_s]}
880 "relates" => {:operator => '=', :values => [issue.id.to_s]}
881 }
881 }
882 assert_equal [issue1], find_issues_with_query(query)
882 assert_equal [issue1], find_issues_with_query(query)
883 end
883 end
884
884
885 def test_filter_on_parent
885 def test_filter_on_parent
886 Issue.delete_all
886 Issue.delete_all
887 parent = Issue.generate_with_descendants!
887 parent = Issue.generate_with_descendants!
888
888
889
889
890 query = IssueQuery.new(:name => '_')
890 query = IssueQuery.new(:name => '_')
891 query.filters = {"parent_id" => {:operator => '=', :values => [parent.id.to_s]}}
891 query.filters = {"parent_id" => {:operator => '=', :values => [parent.id.to_s]}}
892 assert_equal parent.children.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
892 assert_equal parent.children.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
893
893
894 query.filters = {"parent_id" => {:operator => '~', :values => [parent.id.to_s]}}
894 query.filters = {"parent_id" => {:operator => '~', :values => [parent.id.to_s]}}
895 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
895 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
896
896
897 query.filters = {"parent_id" => {:operator => '*', :values => ['']}}
897 query.filters = {"parent_id" => {:operator => '*', :values => ['']}}
898 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
898 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
899
899
900 query.filters = {"parent_id" => {:operator => '!*', :values => ['']}}
900 query.filters = {"parent_id" => {:operator => '!*', :values => ['']}}
901 assert_equal [parent.id], find_issues_with_query(query).map(&:id).sort
901 assert_equal [parent.id], find_issues_with_query(query).map(&:id).sort
902 end
902 end
903
903
904 def test_filter_on_invalid_parent_should_return_no_results
904 def test_filter_on_invalid_parent_should_return_no_results
905 query = IssueQuery.new(:name => '_')
905 query = IssueQuery.new(:name => '_')
906 query.filters = {"parent_id" => {:operator => '=', :values => '99999999999'}}
906 query.filters = {"parent_id" => {:operator => '=', :values => '99999999999'}}
907 assert_equal [], find_issues_with_query(query).map(&:id).sort
907 assert_equal [], find_issues_with_query(query).map(&:id).sort
908
908
909 query.filters = {"parent_id" => {:operator => '~', :values => '99999999999'}}
909 query.filters = {"parent_id" => {:operator => '~', :values => '99999999999'}}
910 assert_equal [], find_issues_with_query(query)
910 assert_equal [], find_issues_with_query(query)
911 end
911 end
912
912
913 def test_filter_on_child
913 def test_filter_on_child
914 Issue.delete_all
914 Issue.delete_all
915 parent = Issue.generate_with_descendants!
915 parent = Issue.generate_with_descendants!
916 child, leaf = parent.children.sort_by(&:id)
916 child, leaf = parent.children.sort_by(&:id)
917 grandchild = child.children.first
917 grandchild = child.children.first
918
918
919
919
920 query = IssueQuery.new(:name => '_')
920 query = IssueQuery.new(:name => '_')
921 query.filters = {"child_id" => {:operator => '=', :values => [grandchild.id.to_s]}}
921 query.filters = {"child_id" => {:operator => '=', :values => [grandchild.id.to_s]}}
922 assert_equal [child.id], find_issues_with_query(query).map(&:id).sort
922 assert_equal [child.id], find_issues_with_query(query).map(&:id).sort
923
923
924 query.filters = {"child_id" => {:operator => '~', :values => [grandchild.id.to_s]}}
924 query.filters = {"child_id" => {:operator => '~', :values => [grandchild.id.to_s]}}
925 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
925 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
926
926
927 query.filters = {"child_id" => {:operator => '*', :values => ['']}}
927 query.filters = {"child_id" => {:operator => '*', :values => ['']}}
928 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
928 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
929
929
930 query.filters = {"child_id" => {:operator => '!*', :values => ['']}}
930 query.filters = {"child_id" => {:operator => '!*', :values => ['']}}
931 assert_equal [grandchild, leaf].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
931 assert_equal [grandchild, leaf].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
932 end
932 end
933
933
934 def test_filter_on_invalid_child_should_return_no_results
934 def test_filter_on_invalid_child_should_return_no_results
935 query = IssueQuery.new(:name => '_')
935 query = IssueQuery.new(:name => '_')
936 query.filters = {"child_id" => {:operator => '=', :values => '99999999999'}}
936 query.filters = {"child_id" => {:operator => '=', :values => '99999999999'}}
937 assert_equal [], find_issues_with_query(query)
937 assert_equal [], find_issues_with_query(query)
938
938
939 query.filters = {"child_id" => {:operator => '~', :values => '99999999999'}}
939 query.filters = {"child_id" => {:operator => '~', :values => '99999999999'}}
940 assert_equal [].map(&:id).sort, find_issues_with_query(query)
940 assert_equal [].map(&:id).sort, find_issues_with_query(query)
941 end
941 end
942
942
943 def test_statement_should_be_nil_with_no_filters
943 def test_statement_should_be_nil_with_no_filters
944 q = IssueQuery.new(:name => '_')
944 q = IssueQuery.new(:name => '_')
945 q.filters = {}
945 q.filters = {}
946
946
947 assert q.valid?
947 assert q.valid?
948 assert_nil q.statement
948 assert_nil q.statement
949 end
949 end
950
950
951 def test_available_filters_as_json_should_include_missing_assigned_to_id_values
952 user = User.generate!
953 with_current_user User.find(1) do
954 q = IssueQuery.new
955 q.filters = {"assigned_to_id" => {:operator => '=', :values => user.id.to_s}}
956
957 filters = q.available_filters_as_json
958 assert_include [user.name, user.id.to_s], filters['assigned_to_id']['values']
959 end
960 end
961
962 def test_available_filters_as_json_should_include_missing_author_id_values
963 user = User.generate!
964 with_current_user User.find(1) do
965 q = IssueQuery.new
966 q.filters = {"author_id" => {:operator => '=', :values => user.id.to_s}}
967
968 filters = q.available_filters_as_json
969 assert_include [user.name, user.id.to_s], filters['author_id']['values']
970 end
971 end
972
951 def test_default_columns
973 def test_default_columns
952 q = IssueQuery.new
974 q = IssueQuery.new
953 assert q.columns.any?
975 assert q.columns.any?
954 assert q.inline_columns.any?
976 assert q.inline_columns.any?
955 assert q.block_columns.empty?
977 assert q.block_columns.empty?
956 end
978 end
957
979
958 def test_set_column_names
980 def test_set_column_names
959 q = IssueQuery.new
981 q = IssueQuery.new
960 q.column_names = ['tracker', :subject, '', 'unknonw_column']
982 q.column_names = ['tracker', :subject, '', 'unknonw_column']
961 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
983 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
962 end
984 end
963
985
964 def test_has_column_should_accept_a_column_name
986 def test_has_column_should_accept_a_column_name
965 q = IssueQuery.new
987 q = IssueQuery.new
966 q.column_names = ['tracker', :subject]
988 q.column_names = ['tracker', :subject]
967 assert q.has_column?(:tracker)
989 assert q.has_column?(:tracker)
968 assert !q.has_column?(:category)
990 assert !q.has_column?(:category)
969 end
991 end
970
992
971 def test_has_column_should_accept_a_column
993 def test_has_column_should_accept_a_column
972 q = IssueQuery.new
994 q = IssueQuery.new
973 q.column_names = ['tracker', :subject]
995 q.column_names = ['tracker', :subject]
974
996
975 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
997 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
976 assert_kind_of QueryColumn, tracker_column
998 assert_kind_of QueryColumn, tracker_column
977 category_column = q.available_columns.detect {|c| c.name==:category}
999 category_column = q.available_columns.detect {|c| c.name==:category}
978 assert_kind_of QueryColumn, category_column
1000 assert_kind_of QueryColumn, category_column
979
1001
980 assert q.has_column?(tracker_column)
1002 assert q.has_column?(tracker_column)
981 assert !q.has_column?(category_column)
1003 assert !q.has_column?(category_column)
982 end
1004 end
983
1005
984 def test_inline_and_block_columns
1006 def test_inline_and_block_columns
985 q = IssueQuery.new
1007 q = IssueQuery.new
986 q.column_names = ['subject', 'description', 'tracker']
1008 q.column_names = ['subject', 'description', 'tracker']
987
1009
988 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
1010 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
989 assert_equal [:description], q.block_columns.map(&:name)
1011 assert_equal [:description], q.block_columns.map(&:name)
990 end
1012 end
991
1013
992 def test_custom_field_columns_should_be_inline
1014 def test_custom_field_columns_should_be_inline
993 q = IssueQuery.new
1015 q = IssueQuery.new
994 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
1016 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
995 assert columns.any?
1017 assert columns.any?
996 assert_nil columns.detect {|column| !column.inline?}
1018 assert_nil columns.detect {|column| !column.inline?}
997 end
1019 end
998
1020
999 def test_query_should_preload_spent_hours
1021 def test_query_should_preload_spent_hours
1000 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
1022 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
1001 assert q.has_column?(:spent_hours)
1023 assert q.has_column?(:spent_hours)
1002 issues = q.issues
1024 issues = q.issues
1003 assert_not_nil issues.first.instance_variable_get("@spent_hours")
1025 assert_not_nil issues.first.instance_variable_get("@spent_hours")
1004 end
1026 end
1005
1027
1006 def test_groupable_columns_should_include_custom_fields
1028 def test_groupable_columns_should_include_custom_fields
1007 q = IssueQuery.new
1029 q = IssueQuery.new
1008 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1030 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1009 assert_not_nil column
1031 assert_not_nil column
1010 assert_kind_of QueryCustomFieldColumn, column
1032 assert_kind_of QueryCustomFieldColumn, column
1011 end
1033 end
1012
1034
1013 def test_groupable_columns_should_not_include_multi_custom_fields
1035 def test_groupable_columns_should_not_include_multi_custom_fields
1014 field = CustomField.find(1)
1036 field = CustomField.find(1)
1015 field.update_attribute :multiple, true
1037 field.update_attribute :multiple, true
1016
1038
1017 q = IssueQuery.new
1039 q = IssueQuery.new
1018 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1040 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1019 assert_nil column
1041 assert_nil column
1020 end
1042 end
1021
1043
1022 def test_groupable_columns_should_include_user_custom_fields
1044 def test_groupable_columns_should_include_user_custom_fields
1023 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
1045 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
1024
1046
1025 q = IssueQuery.new
1047 q = IssueQuery.new
1026 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1048 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1027 end
1049 end
1028
1050
1029 def test_groupable_columns_should_include_version_custom_fields
1051 def test_groupable_columns_should_include_version_custom_fields
1030 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
1052 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
1031
1053
1032 q = IssueQuery.new
1054 q = IssueQuery.new
1033 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1055 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1034 end
1056 end
1035
1057
1036 def test_grouped_with_valid_column
1058 def test_grouped_with_valid_column
1037 q = IssueQuery.new(:group_by => 'status')
1059 q = IssueQuery.new(:group_by => 'status')
1038 assert q.grouped?
1060 assert q.grouped?
1039 assert_not_nil q.group_by_column
1061 assert_not_nil q.group_by_column
1040 assert_equal :status, q.group_by_column.name
1062 assert_equal :status, q.group_by_column.name
1041 assert_not_nil q.group_by_statement
1063 assert_not_nil q.group_by_statement
1042 assert_equal 'status', q.group_by_statement
1064 assert_equal 'status', q.group_by_statement
1043 end
1065 end
1044
1066
1045 def test_grouped_with_invalid_column
1067 def test_grouped_with_invalid_column
1046 q = IssueQuery.new(:group_by => 'foo')
1068 q = IssueQuery.new(:group_by => 'foo')
1047 assert !q.grouped?
1069 assert !q.grouped?
1048 assert_nil q.group_by_column
1070 assert_nil q.group_by_column
1049 assert_nil q.group_by_statement
1071 assert_nil q.group_by_statement
1050 end
1072 end
1051
1073
1052 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
1074 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
1053 with_settings :user_format => 'lastname_comma_firstname' do
1075 with_settings :user_format => 'lastname_comma_firstname' do
1054 q = IssueQuery.new
1076 q = IssueQuery.new
1055 assert q.sortable_columns.has_key?('assigned_to')
1077 assert q.sortable_columns.has_key?('assigned_to')
1056 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
1078 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
1057 end
1079 end
1058 end
1080 end
1059
1081
1060 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
1082 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
1061 with_settings :user_format => 'lastname_comma_firstname' do
1083 with_settings :user_format => 'lastname_comma_firstname' do
1062 q = IssueQuery.new
1084 q = IssueQuery.new
1063 assert q.sortable_columns.has_key?('author')
1085 assert q.sortable_columns.has_key?('author')
1064 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
1086 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
1065 end
1087 end
1066 end
1088 end
1067
1089
1068 def test_sortable_columns_should_include_custom_field
1090 def test_sortable_columns_should_include_custom_field
1069 q = IssueQuery.new
1091 q = IssueQuery.new
1070 assert q.sortable_columns['cf_1']
1092 assert q.sortable_columns['cf_1']
1071 end
1093 end
1072
1094
1073 def test_sortable_columns_should_not_include_multi_custom_field
1095 def test_sortable_columns_should_not_include_multi_custom_field
1074 field = CustomField.find(1)
1096 field = CustomField.find(1)
1075 field.update_attribute :multiple, true
1097 field.update_attribute :multiple, true
1076
1098
1077 q = IssueQuery.new
1099 q = IssueQuery.new
1078 assert !q.sortable_columns['cf_1']
1100 assert !q.sortable_columns['cf_1']
1079 end
1101 end
1080
1102
1081 def test_default_sort
1103 def test_default_sort
1082 q = IssueQuery.new
1104 q = IssueQuery.new
1083 assert_equal [], q.sort_criteria
1105 assert_equal [], q.sort_criteria
1084 end
1106 end
1085
1107
1086 def test_set_sort_criteria_with_hash
1108 def test_set_sort_criteria_with_hash
1087 q = IssueQuery.new
1109 q = IssueQuery.new
1088 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
1110 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
1089 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1111 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1090 end
1112 end
1091
1113
1092 def test_set_sort_criteria_with_array
1114 def test_set_sort_criteria_with_array
1093 q = IssueQuery.new
1115 q = IssueQuery.new
1094 q.sort_criteria = [['priority', 'desc'], 'tracker']
1116 q.sort_criteria = [['priority', 'desc'], 'tracker']
1095 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1117 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1096 end
1118 end
1097
1119
1098 def test_create_query_with_sort
1120 def test_create_query_with_sort
1099 q = IssueQuery.new(:name => 'Sorted')
1121 q = IssueQuery.new(:name => 'Sorted')
1100 q.sort_criteria = [['priority', 'desc'], 'tracker']
1122 q.sort_criteria = [['priority', 'desc'], 'tracker']
1101 assert q.save
1123 assert q.save
1102 q.reload
1124 q.reload
1103 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1125 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1104 end
1126 end
1105
1127
1106 def test_sort_by_string_custom_field_asc
1128 def test_sort_by_string_custom_field_asc
1107 q = IssueQuery.new
1129 q = IssueQuery.new
1108 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1130 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1109 assert c
1131 assert c
1110 assert c.sortable
1132 assert c.sortable
1111 issues = q.issues(:order => "#{c.sortable} ASC")
1133 issues = q.issues(:order => "#{c.sortable} ASC")
1112 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1134 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1113 assert !values.empty?
1135 assert !values.empty?
1114 assert_equal values.sort, values
1136 assert_equal values.sort, values
1115 end
1137 end
1116
1138
1117 def test_sort_by_string_custom_field_desc
1139 def test_sort_by_string_custom_field_desc
1118 q = IssueQuery.new
1140 q = IssueQuery.new
1119 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1141 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1120 assert c
1142 assert c
1121 assert c.sortable
1143 assert c.sortable
1122 issues = q.issues(:order => "#{c.sortable} DESC")
1144 issues = q.issues(:order => "#{c.sortable} DESC")
1123 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1145 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1124 assert !values.empty?
1146 assert !values.empty?
1125 assert_equal values.sort.reverse, values
1147 assert_equal values.sort.reverse, values
1126 end
1148 end
1127
1149
1128 def test_sort_by_float_custom_field_asc
1150 def test_sort_by_float_custom_field_asc
1129 q = IssueQuery.new
1151 q = IssueQuery.new
1130 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
1152 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
1131 assert c
1153 assert c
1132 assert c.sortable
1154 assert c.sortable
1133 issues = q.issues(:order => "#{c.sortable} ASC")
1155 issues = q.issues(:order => "#{c.sortable} ASC")
1134 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
1156 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
1135 assert !values.empty?
1157 assert !values.empty?
1136 assert_equal values.sort, values
1158 assert_equal values.sort, values
1137 end
1159 end
1138
1160
1139 def test_set_totalable_names
1161 def test_set_totalable_names
1140 q = IssueQuery.new
1162 q = IssueQuery.new
1141 q.totalable_names = ['estimated_hours', :spent_hours, '']
1163 q.totalable_names = ['estimated_hours', :spent_hours, '']
1142 assert_equal [:estimated_hours, :spent_hours], q.totalable_columns.map(&:name)
1164 assert_equal [:estimated_hours, :spent_hours], q.totalable_columns.map(&:name)
1143 end
1165 end
1144
1166
1145 def test_totalable_columns_should_default_to_settings
1167 def test_totalable_columns_should_default_to_settings
1146 with_settings :issue_list_default_totals => ['estimated_hours'] do
1168 with_settings :issue_list_default_totals => ['estimated_hours'] do
1147 q = IssueQuery.new
1169 q = IssueQuery.new
1148 assert_equal [:estimated_hours], q.totalable_columns.map(&:name)
1170 assert_equal [:estimated_hours], q.totalable_columns.map(&:name)
1149 end
1171 end
1150 end
1172 end
1151
1173
1152 def test_available_totalable_columns_should_include_estimated_hours
1174 def test_available_totalable_columns_should_include_estimated_hours
1153 q = IssueQuery.new
1175 q = IssueQuery.new
1154 assert_include :estimated_hours, q.available_totalable_columns.map(&:name)
1176 assert_include :estimated_hours, q.available_totalable_columns.map(&:name)
1155 end
1177 end
1156
1178
1157 def test_available_totalable_columns_should_include_spent_hours
1179 def test_available_totalable_columns_should_include_spent_hours
1158 User.current = User.find(1)
1180 User.current = User.find(1)
1159
1181
1160 q = IssueQuery.new
1182 q = IssueQuery.new
1161 assert_include :spent_hours, q.available_totalable_columns.map(&:name)
1183 assert_include :spent_hours, q.available_totalable_columns.map(&:name)
1162 end
1184 end
1163
1185
1164 def test_available_totalable_columns_should_include_int_custom_field
1186 def test_available_totalable_columns_should_include_int_custom_field
1165 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1187 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1166 q = IssueQuery.new
1188 q = IssueQuery.new
1167 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1189 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1168 end
1190 end
1169
1191
1170 def test_available_totalable_columns_should_include_float_custom_field
1192 def test_available_totalable_columns_should_include_float_custom_field
1171 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1193 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1172 q = IssueQuery.new
1194 q = IssueQuery.new
1173 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1195 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1174 end
1196 end
1175
1197
1176 def test_total_for_estimated_hours
1198 def test_total_for_estimated_hours
1177 Issue.delete_all
1199 Issue.delete_all
1178 Issue.generate!(:estimated_hours => 5.5)
1200 Issue.generate!(:estimated_hours => 5.5)
1179 Issue.generate!(:estimated_hours => 1.1)
1201 Issue.generate!(:estimated_hours => 1.1)
1180 Issue.generate!
1202 Issue.generate!
1181
1203
1182 q = IssueQuery.new
1204 q = IssueQuery.new
1183 assert_equal 6.6, q.total_for(:estimated_hours)
1205 assert_equal 6.6, q.total_for(:estimated_hours)
1184 end
1206 end
1185
1207
1186 def test_total_by_group_for_estimated_hours
1208 def test_total_by_group_for_estimated_hours
1187 Issue.delete_all
1209 Issue.delete_all
1188 Issue.generate!(:estimated_hours => 5.5, :assigned_to_id => 2)
1210 Issue.generate!(:estimated_hours => 5.5, :assigned_to_id => 2)
1189 Issue.generate!(:estimated_hours => 1.1, :assigned_to_id => 3)
1211 Issue.generate!(:estimated_hours => 1.1, :assigned_to_id => 3)
1190 Issue.generate!(:estimated_hours => 3.5)
1212 Issue.generate!(:estimated_hours => 3.5)
1191
1213
1192 q = IssueQuery.new(:group_by => 'assigned_to')
1214 q = IssueQuery.new(:group_by => 'assigned_to')
1193 assert_equal(
1215 assert_equal(
1194 {nil => 3.5, User.find(2) => 5.5, User.find(3) => 1.1},
1216 {nil => 3.5, User.find(2) => 5.5, User.find(3) => 1.1},
1195 q.total_by_group_for(:estimated_hours)
1217 q.total_by_group_for(:estimated_hours)
1196 )
1218 )
1197 end
1219 end
1198
1220
1199 def test_total_for_spent_hours
1221 def test_total_for_spent_hours
1200 TimeEntry.delete_all
1222 TimeEntry.delete_all
1201 TimeEntry.generate!(:hours => 5.5)
1223 TimeEntry.generate!(:hours => 5.5)
1202 TimeEntry.generate!(:hours => 1.1)
1224 TimeEntry.generate!(:hours => 1.1)
1203
1225
1204 q = IssueQuery.new
1226 q = IssueQuery.new
1205 assert_equal 6.6, q.total_for(:spent_hours)
1227 assert_equal 6.6, q.total_for(:spent_hours)
1206 end
1228 end
1207
1229
1208 def test_total_by_group_for_spent_hours
1230 def test_total_by_group_for_spent_hours
1209 TimeEntry.delete_all
1231 TimeEntry.delete_all
1210 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1232 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1211 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1233 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1212 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1234 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1213 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1235 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1214
1236
1215 q = IssueQuery.new(:group_by => 'assigned_to')
1237 q = IssueQuery.new(:group_by => 'assigned_to')
1216 assert_equal(
1238 assert_equal(
1217 {User.find(2) => 5.5, User.find(3) => 1.1},
1239 {User.find(2) => 5.5, User.find(3) => 1.1},
1218 q.total_by_group_for(:spent_hours)
1240 q.total_by_group_for(:spent_hours)
1219 )
1241 )
1220 end
1242 end
1221
1243
1222 def test_total_by_project_group_for_spent_hours
1244 def test_total_by_project_group_for_spent_hours
1223 TimeEntry.delete_all
1245 TimeEntry.delete_all
1224 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1246 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1225 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1247 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1226 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1248 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1227 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1249 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1228
1250
1229 q = IssueQuery.new(:group_by => 'project')
1251 q = IssueQuery.new(:group_by => 'project')
1230 assert_equal(
1252 assert_equal(
1231 {Project.find(1) => 6.6},
1253 {Project.find(1) => 6.6},
1232 q.total_by_group_for(:spent_hours)
1254 q.total_by_group_for(:spent_hours)
1233 )
1255 )
1234 end
1256 end
1235
1257
1236 def test_total_for_int_custom_field
1258 def test_total_for_int_custom_field
1237 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1259 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1238 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1260 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1239 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1261 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1240 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1262 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1241
1263
1242 q = IssueQuery.new
1264 q = IssueQuery.new
1243 assert_equal 9, q.total_for("cf_#{field.id}")
1265 assert_equal 9, q.total_for("cf_#{field.id}")
1244 end
1266 end
1245
1267
1246 def test_total_by_group_for_int_custom_field
1268 def test_total_by_group_for_int_custom_field
1247 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1269 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1248 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1270 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1249 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1271 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1250 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1272 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1251 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1273 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1252
1274
1253 q = IssueQuery.new(:group_by => 'assigned_to')
1275 q = IssueQuery.new(:group_by => 'assigned_to')
1254 assert_equal(
1276 assert_equal(
1255 {User.find(2) => 2, User.find(3) => 7},
1277 {User.find(2) => 2, User.find(3) => 7},
1256 q.total_by_group_for("cf_#{field.id}")
1278 q.total_by_group_for("cf_#{field.id}")
1257 )
1279 )
1258 end
1280 end
1259
1281
1260 def test_total_for_float_custom_field
1282 def test_total_for_float_custom_field
1261 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1283 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1262 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2.3')
1284 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2.3')
1263 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1285 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1264 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1286 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1265
1287
1266 q = IssueQuery.new
1288 q = IssueQuery.new
1267 assert_equal 9.3, q.total_for("cf_#{field.id}")
1289 assert_equal 9.3, q.total_for("cf_#{field.id}")
1268 end
1290 end
1269
1291
1270 def test_invalid_query_should_raise_query_statement_invalid_error
1292 def test_invalid_query_should_raise_query_statement_invalid_error
1271 q = IssueQuery.new
1293 q = IssueQuery.new
1272 assert_raise Query::StatementInvalid do
1294 assert_raise Query::StatementInvalid do
1273 q.issues(:conditions => "foo = 1")
1295 q.issues(:conditions => "foo = 1")
1274 end
1296 end
1275 end
1297 end
1276
1298
1277 def test_issue_count
1299 def test_issue_count
1278 q = IssueQuery.new(:name => '_')
1300 q = IssueQuery.new(:name => '_')
1279 issue_count = q.issue_count
1301 issue_count = q.issue_count
1280 assert_equal q.issues.size, issue_count
1302 assert_equal q.issues.size, issue_count
1281 end
1303 end
1282
1304
1283 def test_issue_count_with_archived_issues
1305 def test_issue_count_with_archived_issues
1284 p = Project.generate! do |project|
1306 p = Project.generate! do |project|
1285 project.status = Project::STATUS_ARCHIVED
1307 project.status = Project::STATUS_ARCHIVED
1286 end
1308 end
1287 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
1309 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
1288 assert !i.visible?
1310 assert !i.visible?
1289
1311
1290 test_issue_count
1312 test_issue_count
1291 end
1313 end
1292
1314
1293 def test_issue_count_by_association_group
1315 def test_issue_count_by_association_group
1294 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1316 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1295 count_by_group = q.issue_count_by_group
1317 count_by_group = q.issue_count_by_group
1296 assert_kind_of Hash, count_by_group
1318 assert_kind_of Hash, count_by_group
1297 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1319 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1298 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1320 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1299 assert count_by_group.has_key?(User.find(3))
1321 assert count_by_group.has_key?(User.find(3))
1300 end
1322 end
1301
1323
1302 def test_issue_count_by_list_custom_field_group
1324 def test_issue_count_by_list_custom_field_group
1303 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
1325 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
1304 count_by_group = q.issue_count_by_group
1326 count_by_group = q.issue_count_by_group
1305 assert_kind_of Hash, count_by_group
1327 assert_kind_of Hash, count_by_group
1306 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1328 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1307 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1329 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1308 assert count_by_group.has_key?('MySQL')
1330 assert count_by_group.has_key?('MySQL')
1309 end
1331 end
1310
1332
1311 def test_issue_count_by_date_custom_field_group
1333 def test_issue_count_by_date_custom_field_group
1312 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
1334 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
1313 count_by_group = q.issue_count_by_group
1335 count_by_group = q.issue_count_by_group
1314 assert_kind_of Hash, count_by_group
1336 assert_kind_of Hash, count_by_group
1315 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1337 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1316 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1338 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1317 end
1339 end
1318
1340
1319 def test_issue_count_with_nil_group_only
1341 def test_issue_count_with_nil_group_only
1320 Issue.update_all("assigned_to_id = NULL")
1342 Issue.update_all("assigned_to_id = NULL")
1321
1343
1322 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1344 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1323 count_by_group = q.issue_count_by_group
1345 count_by_group = q.issue_count_by_group
1324 assert_kind_of Hash, count_by_group
1346 assert_kind_of Hash, count_by_group
1325 assert_equal 1, count_by_group.keys.size
1347 assert_equal 1, count_by_group.keys.size
1326 assert_nil count_by_group.keys.first
1348 assert_nil count_by_group.keys.first
1327 end
1349 end
1328
1350
1329 def test_issue_ids
1351 def test_issue_ids
1330 q = IssueQuery.new(:name => '_')
1352 q = IssueQuery.new(:name => '_')
1331 order = "issues.subject, issues.id"
1353 order = "issues.subject, issues.id"
1332 issues = q.issues(:order => order)
1354 issues = q.issues(:order => order)
1333 assert_equal issues.map(&:id), q.issue_ids(:order => order)
1355 assert_equal issues.map(&:id), q.issue_ids(:order => order)
1334 end
1356 end
1335
1357
1336 def test_label_for
1358 def test_label_for
1337 set_language_if_valid 'en'
1359 set_language_if_valid 'en'
1338 q = IssueQuery.new
1360 q = IssueQuery.new
1339 assert_equal 'Assignee', q.label_for('assigned_to_id')
1361 assert_equal 'Assignee', q.label_for('assigned_to_id')
1340 end
1362 end
1341
1363
1342 def test_label_for_fr
1364 def test_label_for_fr
1343 set_language_if_valid 'fr'
1365 set_language_if_valid 'fr'
1344 q = IssueQuery.new
1366 q = IssueQuery.new
1345 assert_equal "Assign\xc3\xa9 \xc3\xa0".force_encoding('UTF-8'), q.label_for('assigned_to_id')
1367 assert_equal "Assign\xc3\xa9 \xc3\xa0".force_encoding('UTF-8'), q.label_for('assigned_to_id')
1346 end
1368 end
1347
1369
1348 def test_editable_by
1370 def test_editable_by
1349 admin = User.find(1)
1371 admin = User.find(1)
1350 manager = User.find(2)
1372 manager = User.find(2)
1351 developer = User.find(3)
1373 developer = User.find(3)
1352
1374
1353 # Public query on project 1
1375 # Public query on project 1
1354 q = IssueQuery.find(1)
1376 q = IssueQuery.find(1)
1355 assert q.editable_by?(admin)
1377 assert q.editable_by?(admin)
1356 assert q.editable_by?(manager)
1378 assert q.editable_by?(manager)
1357 assert !q.editable_by?(developer)
1379 assert !q.editable_by?(developer)
1358
1380
1359 # Private query on project 1
1381 # Private query on project 1
1360 q = IssueQuery.find(2)
1382 q = IssueQuery.find(2)
1361 assert q.editable_by?(admin)
1383 assert q.editable_by?(admin)
1362 assert !q.editable_by?(manager)
1384 assert !q.editable_by?(manager)
1363 assert q.editable_by?(developer)
1385 assert q.editable_by?(developer)
1364
1386
1365 # Private query for all projects
1387 # Private query for all projects
1366 q = IssueQuery.find(3)
1388 q = IssueQuery.find(3)
1367 assert q.editable_by?(admin)
1389 assert q.editable_by?(admin)
1368 assert !q.editable_by?(manager)
1390 assert !q.editable_by?(manager)
1369 assert q.editable_by?(developer)
1391 assert q.editable_by?(developer)
1370
1392
1371 # Public query for all projects
1393 # Public query for all projects
1372 q = IssueQuery.find(4)
1394 q = IssueQuery.find(4)
1373 assert q.editable_by?(admin)
1395 assert q.editable_by?(admin)
1374 assert !q.editable_by?(manager)
1396 assert !q.editable_by?(manager)
1375 assert !q.editable_by?(developer)
1397 assert !q.editable_by?(developer)
1376 end
1398 end
1377
1399
1378 def test_visible_scope
1400 def test_visible_scope
1379 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1401 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1380
1402
1381 assert query_ids.include?(1), 'public query on public project was not visible'
1403 assert query_ids.include?(1), 'public query on public project was not visible'
1382 assert query_ids.include?(4), 'public query for all projects was not visible'
1404 assert query_ids.include?(4), 'public query for all projects was not visible'
1383 assert !query_ids.include?(2), 'private query on public project was visible'
1405 assert !query_ids.include?(2), 'private query on public project was visible'
1384 assert !query_ids.include?(3), 'private query for all projects was visible'
1406 assert !query_ids.include?(3), 'private query for all projects was visible'
1385 assert !query_ids.include?(7), 'public query on private project was visible'
1407 assert !query_ids.include?(7), 'public query on private project was visible'
1386 end
1408 end
1387
1409
1388 def test_query_with_public_visibility_should_be_visible_to_anyone
1410 def test_query_with_public_visibility_should_be_visible_to_anyone
1389 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC)
1411 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC)
1390
1412
1391 assert q.visible?(User.anonymous)
1413 assert q.visible?(User.anonymous)
1392 assert IssueQuery.visible(User.anonymous).find_by_id(q.id)
1414 assert IssueQuery.visible(User.anonymous).find_by_id(q.id)
1393
1415
1394 assert q.visible?(User.find(7))
1416 assert q.visible?(User.find(7))
1395 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1417 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1396
1418
1397 assert q.visible?(User.find(2))
1419 assert q.visible?(User.find(2))
1398 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1420 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1399
1421
1400 assert q.visible?(User.find(1))
1422 assert q.visible?(User.find(1))
1401 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1423 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1402 end
1424 end
1403
1425
1404 def test_query_with_roles_visibility_should_be_visible_to_user_with_role
1426 def test_query_with_roles_visibility_should_be_visible_to_user_with_role
1405 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2])
1427 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2])
1406
1428
1407 assert !q.visible?(User.anonymous)
1429 assert !q.visible?(User.anonymous)
1408 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1430 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1409
1431
1410 assert !q.visible?(User.find(7))
1432 assert !q.visible?(User.find(7))
1411 assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id)
1433 assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id)
1412
1434
1413 assert q.visible?(User.find(2))
1435 assert q.visible?(User.find(2))
1414 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1436 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1415
1437
1416 assert q.visible?(User.find(1))
1438 assert q.visible?(User.find(1))
1417 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1439 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1418 end
1440 end
1419
1441
1420 def test_query_with_private_visibility_should_be_visible_to_owner
1442 def test_query_with_private_visibility_should_be_visible_to_owner
1421 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7))
1443 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7))
1422
1444
1423 assert !q.visible?(User.anonymous)
1445 assert !q.visible?(User.anonymous)
1424 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1446 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1425
1447
1426 assert q.visible?(User.find(7))
1448 assert q.visible?(User.find(7))
1427 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1449 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1428
1450
1429 assert !q.visible?(User.find(2))
1451 assert !q.visible?(User.find(2))
1430 assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id)
1452 assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id)
1431
1453
1432 assert q.visible?(User.find(1))
1454 assert q.visible?(User.find(1))
1433 assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id)
1455 assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id)
1434 end
1456 end
1435
1457
1436 test "#available_filters should include users of visible projects in cross-project view" do
1458 test "#available_filters should include users of visible projects in cross-project view" do
1437 users = IssueQuery.new.available_filters["assigned_to_id"]
1459 users = IssueQuery.new.available_filters["assigned_to_id"]
1438 assert_not_nil users
1460 assert_not_nil users
1439 assert users[:values].map{|u|u[1]}.include?("3")
1461 assert users[:values].map{|u|u[1]}.include?("3")
1440 end
1462 end
1441
1463
1442 test "#available_filters should include users of subprojects" do
1464 test "#available_filters should include users of subprojects" do
1443 user1 = User.generate!
1465 user1 = User.generate!
1444 user2 = User.generate!
1466 user2 = User.generate!
1445 project = Project.find(1)
1467 project = Project.find(1)
1446 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1468 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1447
1469
1448 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1470 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1449 assert_not_nil users
1471 assert_not_nil users
1450 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1472 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1451 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1473 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1452 end
1474 end
1453
1475
1454 test "#available_filters should include visible projects in cross-project view" do
1476 test "#available_filters should include visible projects in cross-project view" do
1455 projects = IssueQuery.new.available_filters["project_id"]
1477 projects = IssueQuery.new.available_filters["project_id"]
1456 assert_not_nil projects
1478 assert_not_nil projects
1457 assert projects[:values].map{|u|u[1]}.include?("1")
1479 assert projects[:values].map{|u|u[1]}.include?("1")
1458 end
1480 end
1459
1481
1460 test "#available_filters should include 'member_of_group' filter" do
1482 test "#available_filters should include 'member_of_group' filter" do
1461 query = IssueQuery.new
1483 query = IssueQuery.new
1462 assert query.available_filters.keys.include?("member_of_group")
1484 assert query.available_filters.keys.include?("member_of_group")
1463 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1485 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1464 assert query.available_filters["member_of_group"][:values].present?
1486 assert query.available_filters["member_of_group"][:values].present?
1465 assert_equal Group.givable.sort.map {|g| [g.name, g.id.to_s]},
1487 assert_equal Group.givable.sort.map {|g| [g.name, g.id.to_s]},
1466 query.available_filters["member_of_group"][:values].sort
1488 query.available_filters["member_of_group"][:values].sort
1467 end
1489 end
1468
1490
1469 test "#available_filters should include 'assigned_to_role' filter" do
1491 test "#available_filters should include 'assigned_to_role' filter" do
1470 query = IssueQuery.new
1492 query = IssueQuery.new
1471 assert query.available_filters.keys.include?("assigned_to_role")
1493 assert query.available_filters.keys.include?("assigned_to_role")
1472 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1494 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1473
1495
1474 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1496 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1475 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1497 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1476 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1498 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1477
1499
1478 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1500 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1479 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1501 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1480 end
1502 end
1481
1503
1482 def test_available_filters_should_include_custom_field_according_to_user_visibility
1504 def test_available_filters_should_include_custom_field_according_to_user_visibility
1483 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1505 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1484 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1506 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1485
1507
1486 with_current_user User.find(3) do
1508 with_current_user User.find(3) do
1487 query = IssueQuery.new
1509 query = IssueQuery.new
1488 assert_include "cf_#{visible_field.id}", query.available_filters.keys
1510 assert_include "cf_#{visible_field.id}", query.available_filters.keys
1489 assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys
1511 assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys
1490 end
1512 end
1491 end
1513 end
1492
1514
1493 def test_available_columns_should_include_custom_field_according_to_user_visibility
1515 def test_available_columns_should_include_custom_field_according_to_user_visibility
1494 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1516 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1495 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1517 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1496
1518
1497 with_current_user User.find(3) do
1519 with_current_user User.find(3) do
1498 query = IssueQuery.new
1520 query = IssueQuery.new
1499 assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name)
1521 assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name)
1500 assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name)
1522 assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name)
1501 end
1523 end
1502 end
1524 end
1503
1525
1504 def setup_member_of_group
1526 def setup_member_of_group
1505 Group.destroy_all # No fixtures
1527 Group.destroy_all # No fixtures
1506 @user_in_group = User.generate!
1528 @user_in_group = User.generate!
1507 @second_user_in_group = User.generate!
1529 @second_user_in_group = User.generate!
1508 @user_in_group2 = User.generate!
1530 @user_in_group2 = User.generate!
1509 @user_not_in_group = User.generate!
1531 @user_not_in_group = User.generate!
1510
1532
1511 @group = Group.generate!.reload
1533 @group = Group.generate!.reload
1512 @group.users << @user_in_group
1534 @group.users << @user_in_group
1513 @group.users << @second_user_in_group
1535 @group.users << @second_user_in_group
1514
1536
1515 @group2 = Group.generate!.reload
1537 @group2 = Group.generate!.reload
1516 @group2.users << @user_in_group2
1538 @group2.users << @user_in_group2
1517
1539
1518 @query = IssueQuery.new(:name => '_')
1540 @query = IssueQuery.new(:name => '_')
1519 end
1541 end
1520
1542
1521 test "member_of_group filter should search assigned to for users in the group" do
1543 test "member_of_group filter should search assigned to for users in the group" do
1522 setup_member_of_group
1544 setup_member_of_group
1523 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1545 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1524
1546
1525 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')"
1547 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')"
1526 assert_find_issues_with_query_is_successful @query
1548 assert_find_issues_with_query_is_successful @query
1527 end
1549 end
1528
1550
1529 test "member_of_group filter should search not assigned to any group member (none)" do
1551 test "member_of_group filter should search not assigned to any group member (none)" do
1530 setup_member_of_group
1552 setup_member_of_group
1531 @query.add_filter('member_of_group', '!*', [''])
1553 @query.add_filter('member_of_group', '!*', [''])
1532
1554
1533 # Users not in a group
1555 # Users not in a group
1534 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1556 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1535 assert_find_issues_with_query_is_successful @query
1557 assert_find_issues_with_query_is_successful @query
1536 end
1558 end
1537
1559
1538 test "member_of_group filter should search assigned to any group member (all)" do
1560 test "member_of_group filter should search assigned to any group member (all)" do
1539 setup_member_of_group
1561 setup_member_of_group
1540 @query.add_filter('member_of_group', '*', [''])
1562 @query.add_filter('member_of_group', '*', [''])
1541
1563
1542 # Only users in a group
1564 # Only users in a group
1543 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1565 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')"
1544 assert_find_issues_with_query_is_successful @query
1566 assert_find_issues_with_query_is_successful @query
1545 end
1567 end
1546
1568
1547 test "member_of_group filter should return an empty set with = empty group" do
1569 test "member_of_group filter should return an empty set with = empty group" do
1548 setup_member_of_group
1570 setup_member_of_group
1549 @empty_group = Group.generate!
1571 @empty_group = Group.generate!
1550 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1572 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1551
1573
1552 assert_equal [], find_issues_with_query(@query)
1574 assert_equal [], find_issues_with_query(@query)
1553 end
1575 end
1554
1576
1555 test "member_of_group filter should return issues with ! empty group" do
1577 test "member_of_group filter should return issues with ! empty group" do
1556 setup_member_of_group
1578 setup_member_of_group
1557 @empty_group = Group.generate!
1579 @empty_group = Group.generate!
1558 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1580 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1559
1581
1560 assert_find_issues_with_query_is_successful @query
1582 assert_find_issues_with_query_is_successful @query
1561 end
1583 end
1562
1584
1563 def setup_assigned_to_role
1585 def setup_assigned_to_role
1564 @manager_role = Role.find_by_name('Manager')
1586 @manager_role = Role.find_by_name('Manager')
1565 @developer_role = Role.find_by_name('Developer')
1587 @developer_role = Role.find_by_name('Developer')
1566
1588
1567 @project = Project.generate!
1589 @project = Project.generate!
1568 @manager = User.generate!
1590 @manager = User.generate!
1569 @developer = User.generate!
1591 @developer = User.generate!
1570 @boss = User.generate!
1592 @boss = User.generate!
1571 @guest = User.generate!
1593 @guest = User.generate!
1572 User.add_to_project(@manager, @project, @manager_role)
1594 User.add_to_project(@manager, @project, @manager_role)
1573 User.add_to_project(@developer, @project, @developer_role)
1595 User.add_to_project(@developer, @project, @developer_role)
1574 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1596 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1575
1597
1576 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1598 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1577 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1599 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1578 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1600 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1579 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1601 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1580 @issue5 = Issue.generate!(:project => @project)
1602 @issue5 = Issue.generate!(:project => @project)
1581
1603
1582 @query = IssueQuery.new(:name => '_', :project => @project)
1604 @query = IssueQuery.new(:name => '_', :project => @project)
1583 end
1605 end
1584
1606
1585 test "assigned_to_role filter should search assigned to for users with the Role" do
1607 test "assigned_to_role filter should search assigned to for users with the Role" do
1586 setup_assigned_to_role
1608 setup_assigned_to_role
1587 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1609 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1588
1610
1589 assert_query_result [@issue1, @issue3], @query
1611 assert_query_result [@issue1, @issue3], @query
1590 end
1612 end
1591
1613
1592 test "assigned_to_role filter should search assigned to for users with the Role on the issue project" do
1614 test "assigned_to_role filter should search assigned to for users with the Role on the issue project" do
1593 setup_assigned_to_role
1615 setup_assigned_to_role
1594 other_project = Project.generate!
1616 other_project = Project.generate!
1595 User.add_to_project(@developer, other_project, @manager_role)
1617 User.add_to_project(@developer, other_project, @manager_role)
1596 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1618 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1597
1619
1598 assert_query_result [@issue1, @issue3], @query
1620 assert_query_result [@issue1, @issue3], @query
1599 end
1621 end
1600
1622
1601 test "assigned_to_role filter should return an empty set with empty role" do
1623 test "assigned_to_role filter should return an empty set with empty role" do
1602 setup_assigned_to_role
1624 setup_assigned_to_role
1603 @empty_role = Role.generate!
1625 @empty_role = Role.generate!
1604 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1626 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1605
1627
1606 assert_query_result [], @query
1628 assert_query_result [], @query
1607 end
1629 end
1608
1630
1609 test "assigned_to_role filter should search assigned to for users without the Role" do
1631 test "assigned_to_role filter should search assigned to for users without the Role" do
1610 setup_assigned_to_role
1632 setup_assigned_to_role
1611 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1633 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1612
1634
1613 assert_query_result [@issue2, @issue4, @issue5], @query
1635 assert_query_result [@issue2, @issue4, @issue5], @query
1614 end
1636 end
1615
1637
1616 test "assigned_to_role filter should search assigned to for users not assigned to any Role (none)" do
1638 test "assigned_to_role filter should search assigned to for users not assigned to any Role (none)" do
1617 setup_assigned_to_role
1639 setup_assigned_to_role
1618 @query.add_filter('assigned_to_role', '!*', [''])
1640 @query.add_filter('assigned_to_role', '!*', [''])
1619
1641
1620 assert_query_result [@issue4, @issue5], @query
1642 assert_query_result [@issue4, @issue5], @query
1621 end
1643 end
1622
1644
1623 test "assigned_to_role filter should search assigned to for users assigned to any Role (all)" do
1645 test "assigned_to_role filter should search assigned to for users assigned to any Role (all)" do
1624 setup_assigned_to_role
1646 setup_assigned_to_role
1625 @query.add_filter('assigned_to_role', '*', [''])
1647 @query.add_filter('assigned_to_role', '*', [''])
1626
1648
1627 assert_query_result [@issue1, @issue2, @issue3], @query
1649 assert_query_result [@issue1, @issue2, @issue3], @query
1628 end
1650 end
1629
1651
1630 test "assigned_to_role filter should return issues with ! empty role" do
1652 test "assigned_to_role filter should return issues with ! empty role" do
1631 setup_assigned_to_role
1653 setup_assigned_to_role
1632 @empty_role = Role.generate!
1654 @empty_role = Role.generate!
1633 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1655 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1634
1656
1635 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1657 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1636 end
1658 end
1637
1659
1638 def test_query_column_should_accept_a_symbol_as_caption
1660 def test_query_column_should_accept_a_symbol_as_caption
1639 set_language_if_valid 'en'
1661 set_language_if_valid 'en'
1640 c = QueryColumn.new('foo', :caption => :general_text_Yes)
1662 c = QueryColumn.new('foo', :caption => :general_text_Yes)
1641 assert_equal 'Yes', c.caption
1663 assert_equal 'Yes', c.caption
1642 end
1664 end
1643
1665
1644 def test_query_column_should_accept_a_proc_as_caption
1666 def test_query_column_should_accept_a_proc_as_caption
1645 c = QueryColumn.new('foo', :caption => lambda {'Foo'})
1667 c = QueryColumn.new('foo', :caption => lambda {'Foo'})
1646 assert_equal 'Foo', c.caption
1668 assert_equal 'Foo', c.caption
1647 end
1669 end
1648 end
1670 end
General Comments 0
You need to be logged in to leave comments. Login now