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