##// END OF EJS Templates
Refactor: makes issue id a regular QueryColumn....
Jean-Philippe Lang -
r11217:9b1ebd6808d2
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,393 +1,393
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def issue_list(issues, &block)
23 def issue_list(issues, &block)
24 ancestors = []
24 ancestors = []
25 issues.each do |issue|
25 issues.each do |issue|
26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 ancestors.pop
27 ancestors.pop
28 end
28 end
29 yield issue, ancestors.size
29 yield issue, ancestors.size
30 ancestors << issue unless issue.leaf?
30 ancestors << issue unless issue.leaf?
31 end
31 end
32 end
32 end
33
33
34 # Renders a HTML/CSS tooltip
34 # Renders a HTML/CSS tooltip
35 #
35 #
36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
37 # that contains this method wrapped in a span with the class of "tip"
37 # that contains this method wrapped in a span with the class of "tip"
38 #
38 #
39 # <div class="tooltip"><%= link_to_issue(issue) %>
39 # <div class="tooltip"><%= link_to_issue(issue) %>
40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
41 # </div>
41 # </div>
42 #
42 #
43 def render_issue_tooltip(issue)
43 def render_issue_tooltip(issue)
44 @cached_label_status ||= l(:field_status)
44 @cached_label_status ||= l(:field_status)
45 @cached_label_start_date ||= l(:field_start_date)
45 @cached_label_start_date ||= l(:field_start_date)
46 @cached_label_due_date ||= l(:field_due_date)
46 @cached_label_due_date ||= l(:field_due_date)
47 @cached_label_assigned_to ||= l(:field_assigned_to)
47 @cached_label_assigned_to ||= l(:field_assigned_to)
48 @cached_label_priority ||= l(:field_priority)
48 @cached_label_priority ||= l(:field_priority)
49 @cached_label_project ||= l(:field_project)
49 @cached_label_project ||= l(:field_project)
50
50
51 link_to_issue(issue) + "<br /><br />".html_safe +
51 link_to_issue(issue) + "<br /><br />".html_safe +
52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 end
58 end
59
59
60 def issue_heading(issue)
60 def issue_heading(issue)
61 h("#{issue.tracker} ##{issue.id}")
61 h("#{issue.tracker} ##{issue.id}")
62 end
62 end
63
63
64 def render_issue_subject_with_tree(issue)
64 def render_issue_subject_with_tree(issue)
65 s = ''
65 s = ''
66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
67 ancestors.each do |ancestor|
67 ancestors.each do |ancestor|
68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
69 end
69 end
70 s << '<div>'
70 s << '<div>'
71 subject = h(issue.subject)
71 subject = h(issue.subject)
72 if issue.is_private?
72 if issue.is_private?
73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
74 end
74 end
75 s << content_tag('h3', subject)
75 s << content_tag('h3', subject)
76 s << '</div>' * (ancestors.size + 1)
76 s << '</div>' * (ancestors.size + 1)
77 s.html_safe
77 s.html_safe
78 end
78 end
79
79
80 def render_descendants_tree(issue)
80 def render_descendants_tree(issue)
81 s = '<form><table class="list issues">'
81 s = '<form><table class="list issues">'
82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
83 css = "issue issue-#{child.id} hascontextmenu"
83 css = "issue issue-#{child.id} hascontextmenu"
84 css << " idnt idnt-#{level}" if level > 0
84 css << " idnt idnt-#{level}" if level > 0
85 s << content_tag('tr',
85 s << content_tag('tr',
86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
88 content_tag('td', h(child.status)) +
88 content_tag('td', h(child.status)) +
89 content_tag('td', link_to_user(child.assigned_to)) +
89 content_tag('td', link_to_user(child.assigned_to)) +
90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
91 :class => css)
91 :class => css)
92 end
92 end
93 s << '</table></form>'
93 s << '</table></form>'
94 s.html_safe
94 s.html_safe
95 end
95 end
96
96
97 # Returns a link for adding a new subtask to the given issue
97 # Returns a link for adding a new subtask to the given issue
98 def link_to_new_subtask(issue)
98 def link_to_new_subtask(issue)
99 attrs = {
99 attrs = {
100 :tracker_id => issue.tracker,
100 :tracker_id => issue.tracker,
101 :parent_issue_id => issue
101 :parent_issue_id => issue
102 }
102 }
103 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
103 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
104 end
104 end
105
105
106 class IssueFieldsRows
106 class IssueFieldsRows
107 include ActionView::Helpers::TagHelper
107 include ActionView::Helpers::TagHelper
108
108
109 def initialize
109 def initialize
110 @left = []
110 @left = []
111 @right = []
111 @right = []
112 end
112 end
113
113
114 def left(*args)
114 def left(*args)
115 args.any? ? @left << cells(*args) : @left
115 args.any? ? @left << cells(*args) : @left
116 end
116 end
117
117
118 def right(*args)
118 def right(*args)
119 args.any? ? @right << cells(*args) : @right
119 args.any? ? @right << cells(*args) : @right
120 end
120 end
121
121
122 def size
122 def size
123 @left.size > @right.size ? @left.size : @right.size
123 @left.size > @right.size ? @left.size : @right.size
124 end
124 end
125
125
126 def to_html
126 def to_html
127 html = ''.html_safe
127 html = ''.html_safe
128 blank = content_tag('th', '') + content_tag('td', '')
128 blank = content_tag('th', '') + content_tag('td', '')
129 size.times do |i|
129 size.times do |i|
130 left = @left[i] || blank
130 left = @left[i] || blank
131 right = @right[i] || blank
131 right = @right[i] || blank
132 html << content_tag('tr', left + right)
132 html << content_tag('tr', left + right)
133 end
133 end
134 html
134 html
135 end
135 end
136
136
137 def cells(label, text, options={})
137 def cells(label, text, options={})
138 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
138 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
139 end
139 end
140 end
140 end
141
141
142 def issue_fields_rows
142 def issue_fields_rows
143 r = IssueFieldsRows.new
143 r = IssueFieldsRows.new
144 yield r
144 yield r
145 r.to_html
145 r.to_html
146 end
146 end
147
147
148 def render_custom_fields_rows(issue)
148 def render_custom_fields_rows(issue)
149 return if issue.custom_field_values.empty?
149 return if issue.custom_field_values.empty?
150 ordered_values = []
150 ordered_values = []
151 half = (issue.custom_field_values.size / 2.0).ceil
151 half = (issue.custom_field_values.size / 2.0).ceil
152 half.times do |i|
152 half.times do |i|
153 ordered_values << issue.custom_field_values[i]
153 ordered_values << issue.custom_field_values[i]
154 ordered_values << issue.custom_field_values[i + half]
154 ordered_values << issue.custom_field_values[i + half]
155 end
155 end
156 s = "<tr>\n"
156 s = "<tr>\n"
157 n = 0
157 n = 0
158 ordered_values.compact.each do |value|
158 ordered_values.compact.each do |value|
159 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
159 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
160 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
160 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
161 n += 1
161 n += 1
162 end
162 end
163 s << "</tr>\n"
163 s << "</tr>\n"
164 s.html_safe
164 s.html_safe
165 end
165 end
166
166
167 def issues_destroy_confirmation_message(issues)
167 def issues_destroy_confirmation_message(issues)
168 issues = [issues] unless issues.is_a?(Array)
168 issues = [issues] unless issues.is_a?(Array)
169 message = l(:text_issues_destroy_confirmation)
169 message = l(:text_issues_destroy_confirmation)
170 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
170 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
171 if descendant_count > 0
171 if descendant_count > 0
172 issues.each do |issue|
172 issues.each do |issue|
173 next if issue.root?
173 next if issue.root?
174 issues.each do |other_issue|
174 issues.each do |other_issue|
175 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
175 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
176 end
176 end
177 end
177 end
178 if descendant_count > 0
178 if descendant_count > 0
179 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
179 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
180 end
180 end
181 end
181 end
182 message
182 message
183 end
183 end
184
184
185 def sidebar_queries
185 def sidebar_queries
186 unless @sidebar_queries
186 unless @sidebar_queries
187 @sidebar_queries = IssueQuery.visible.all(
187 @sidebar_queries = IssueQuery.visible.all(
188 :order => "#{Query.table_name}.name ASC",
188 :order => "#{Query.table_name}.name ASC",
189 # Project specific queries and global queries
189 # Project specific queries and global queries
190 :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
190 :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
191 )
191 )
192 end
192 end
193 @sidebar_queries
193 @sidebar_queries
194 end
194 end
195
195
196 def query_links(title, queries)
196 def query_links(title, queries)
197 # links to #index on issues/show
197 # links to #index on issues/show
198 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
198 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
199
199
200 content_tag('h3', h(title)) +
200 content_tag('h3', h(title)) +
201 queries.collect {|query|
201 queries.collect {|query|
202 css = 'query'
202 css = 'query'
203 css << ' selected' if query == @query
203 css << ' selected' if query == @query
204 link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
204 link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
205 }.join('<br />').html_safe
205 }.join('<br />').html_safe
206 end
206 end
207
207
208 def render_sidebar_queries
208 def render_sidebar_queries
209 out = ''.html_safe
209 out = ''.html_safe
210 queries = sidebar_queries.select {|q| !q.is_public?}
210 queries = sidebar_queries.select {|q| !q.is_public?}
211 out << query_links(l(:label_my_queries), queries) if queries.any?
211 out << query_links(l(:label_my_queries), queries) if queries.any?
212 queries = sidebar_queries.select {|q| q.is_public?}
212 queries = sidebar_queries.select {|q| q.is_public?}
213 out << query_links(l(:label_query_plural), queries) if queries.any?
213 out << query_links(l(:label_query_plural), queries) if queries.any?
214 out
214 out
215 end
215 end
216
216
217 # Returns the textual representation of a journal details
217 # Returns the textual representation of a journal details
218 # as an array of strings
218 # as an array of strings
219 def details_to_strings(details, no_html=false, options={})
219 def details_to_strings(details, no_html=false, options={})
220 options[:only_path] = (options[:only_path] == false ? false : true)
220 options[:only_path] = (options[:only_path] == false ? false : true)
221 strings = []
221 strings = []
222 values_by_field = {}
222 values_by_field = {}
223 details.each do |detail|
223 details.each do |detail|
224 if detail.property == 'cf'
224 if detail.property == 'cf'
225 field_id = detail.prop_key
225 field_id = detail.prop_key
226 field = CustomField.find_by_id(field_id)
226 field = CustomField.find_by_id(field_id)
227 if field && field.multiple?
227 if field && field.multiple?
228 values_by_field[field_id] ||= {:added => [], :deleted => []}
228 values_by_field[field_id] ||= {:added => [], :deleted => []}
229 if detail.old_value
229 if detail.old_value
230 values_by_field[field_id][:deleted] << detail.old_value
230 values_by_field[field_id][:deleted] << detail.old_value
231 end
231 end
232 if detail.value
232 if detail.value
233 values_by_field[field_id][:added] << detail.value
233 values_by_field[field_id][:added] << detail.value
234 end
234 end
235 next
235 next
236 end
236 end
237 end
237 end
238 strings << show_detail(detail, no_html, options)
238 strings << show_detail(detail, no_html, options)
239 end
239 end
240 values_by_field.each do |field_id, changes|
240 values_by_field.each do |field_id, changes|
241 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
241 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
242 if changes[:added].any?
242 if changes[:added].any?
243 detail.value = changes[:added]
243 detail.value = changes[:added]
244 strings << show_detail(detail, no_html, options)
244 strings << show_detail(detail, no_html, options)
245 elsif changes[:deleted].any?
245 elsif changes[:deleted].any?
246 detail.old_value = changes[:deleted]
246 detail.old_value = changes[:deleted]
247 strings << show_detail(detail, no_html, options)
247 strings << show_detail(detail, no_html, options)
248 end
248 end
249 end
249 end
250 strings
250 strings
251 end
251 end
252
252
253 # Returns the textual representation of a single journal detail
253 # Returns the textual representation of a single journal detail
254 def show_detail(detail, no_html=false, options={})
254 def show_detail(detail, no_html=false, options={})
255 multiple = false
255 multiple = false
256 case detail.property
256 case detail.property
257 when 'attr'
257 when 'attr'
258 field = detail.prop_key.to_s.gsub(/\_id$/, "")
258 field = detail.prop_key.to_s.gsub(/\_id$/, "")
259 label = l(("field_" + field).to_sym)
259 label = l(("field_" + field).to_sym)
260 case detail.prop_key
260 case detail.prop_key
261 when 'due_date', 'start_date'
261 when 'due_date', 'start_date'
262 value = format_date(detail.value.to_date) if detail.value
262 value = format_date(detail.value.to_date) if detail.value
263 old_value = format_date(detail.old_value.to_date) if detail.old_value
263 old_value = format_date(detail.old_value.to_date) if detail.old_value
264
264
265 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
265 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
266 'priority_id', 'category_id', 'fixed_version_id'
266 'priority_id', 'category_id', 'fixed_version_id'
267 value = find_name_by_reflection(field, detail.value)
267 value = find_name_by_reflection(field, detail.value)
268 old_value = find_name_by_reflection(field, detail.old_value)
268 old_value = find_name_by_reflection(field, detail.old_value)
269
269
270 when 'estimated_hours'
270 when 'estimated_hours'
271 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
271 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
272 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
272 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
273
273
274 when 'parent_id'
274 when 'parent_id'
275 label = l(:field_parent_issue)
275 label = l(:field_parent_issue)
276 value = "##{detail.value}" unless detail.value.blank?
276 value = "##{detail.value}" unless detail.value.blank?
277 old_value = "##{detail.old_value}" unless detail.old_value.blank?
277 old_value = "##{detail.old_value}" unless detail.old_value.blank?
278
278
279 when 'is_private'
279 when 'is_private'
280 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
280 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
281 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
281 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
282 end
282 end
283 when 'cf'
283 when 'cf'
284 custom_field = CustomField.find_by_id(detail.prop_key)
284 custom_field = CustomField.find_by_id(detail.prop_key)
285 if custom_field
285 if custom_field
286 multiple = custom_field.multiple?
286 multiple = custom_field.multiple?
287 label = custom_field.name
287 label = custom_field.name
288 value = format_value(detail.value, custom_field.field_format) if detail.value
288 value = format_value(detail.value, custom_field.field_format) if detail.value
289 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
289 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
290 end
290 end
291 when 'attachment'
291 when 'attachment'
292 label = l(:label_attachment)
292 label = l(:label_attachment)
293 end
293 end
294 call_hook(:helper_issues_show_detail_after_setting,
294 call_hook(:helper_issues_show_detail_after_setting,
295 {:detail => detail, :label => label, :value => value, :old_value => old_value })
295 {:detail => detail, :label => label, :value => value, :old_value => old_value })
296
296
297 label ||= detail.prop_key
297 label ||= detail.prop_key
298 value ||= detail.value
298 value ||= detail.value
299 old_value ||= detail.old_value
299 old_value ||= detail.old_value
300
300
301 unless no_html
301 unless no_html
302 label = content_tag('strong', label)
302 label = content_tag('strong', label)
303 old_value = content_tag("i", h(old_value)) if detail.old_value
303 old_value = content_tag("i", h(old_value)) if detail.old_value
304 old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank?
304 old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank?
305 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
305 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
306 # Link to the attachment if it has not been removed
306 # Link to the attachment if it has not been removed
307 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
307 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
308 if options[:only_path] != false && atta.is_text?
308 if options[:only_path] != false && atta.is_text?
309 value += link_to(
309 value += link_to(
310 image_tag('magnifier.png'),
310 image_tag('magnifier.png'),
311 :controller => 'attachments', :action => 'show',
311 :controller => 'attachments', :action => 'show',
312 :id => atta, :filename => atta.filename
312 :id => atta, :filename => atta.filename
313 )
313 )
314 end
314 end
315 else
315 else
316 value = content_tag("i", h(value)) if value
316 value = content_tag("i", h(value)) if value
317 end
317 end
318 end
318 end
319
319
320 if detail.property == 'attr' && detail.prop_key == 'description'
320 if detail.property == 'attr' && detail.prop_key == 'description'
321 s = l(:text_journal_changed_no_detail, :label => label)
321 s = l(:text_journal_changed_no_detail, :label => label)
322 unless no_html
322 unless no_html
323 diff_link = link_to 'diff',
323 diff_link = link_to 'diff',
324 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
324 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
325 :detail_id => detail.id, :only_path => options[:only_path]},
325 :detail_id => detail.id, :only_path => options[:only_path]},
326 :title => l(:label_view_diff)
326 :title => l(:label_view_diff)
327 s << " (#{ diff_link })"
327 s << " (#{ diff_link })"
328 end
328 end
329 s.html_safe
329 s.html_safe
330 elsif detail.value.present?
330 elsif detail.value.present?
331 case detail.property
331 case detail.property
332 when 'attr', 'cf'
332 when 'attr', 'cf'
333 if detail.old_value.present?
333 if detail.old_value.present?
334 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
334 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
335 elsif multiple
335 elsif multiple
336 l(:text_journal_added, :label => label, :value => value).html_safe
336 l(:text_journal_added, :label => label, :value => value).html_safe
337 else
337 else
338 l(:text_journal_set_to, :label => label, :value => value).html_safe
338 l(:text_journal_set_to, :label => label, :value => value).html_safe
339 end
339 end
340 when 'attachment'
340 when 'attachment'
341 l(:text_journal_added, :label => label, :value => value).html_safe
341 l(:text_journal_added, :label => label, :value => value).html_safe
342 end
342 end
343 else
343 else
344 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
344 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
345 end
345 end
346 end
346 end
347
347
348 # Find the name of an associated record stored in the field attribute
348 # Find the name of an associated record stored in the field attribute
349 def find_name_by_reflection(field, id)
349 def find_name_by_reflection(field, id)
350 unless id.present?
350 unless id.present?
351 return nil
351 return nil
352 end
352 end
353 association = Issue.reflect_on_association(field.to_sym)
353 association = Issue.reflect_on_association(field.to_sym)
354 if association
354 if association
355 record = association.class_name.constantize.find_by_id(id)
355 record = association.class_name.constantize.find_by_id(id)
356 return record.name if record
356 return record.name if record
357 end
357 end
358 end
358 end
359
359
360 # Renders issue children recursively
360 # Renders issue children recursively
361 def render_api_issue_children(issue, api)
361 def render_api_issue_children(issue, api)
362 return if issue.leaf?
362 return if issue.leaf?
363 api.array :children do
363 api.array :children do
364 issue.children.each do |child|
364 issue.children.each do |child|
365 api.issue(:id => child.id) do
365 api.issue(:id => child.id) do
366 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
366 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
367 api.subject child.subject
367 api.subject child.subject
368 render_api_issue_children(child, api)
368 render_api_issue_children(child, api)
369 end
369 end
370 end
370 end
371 end
371 end
372 end
372 end
373
373
374 def issues_to_csv(issues, project, query, options={})
374 def issues_to_csv(issues, project, query, options={})
375 encoding = l(:general_csv_encoding)
375 encoding = l(:general_csv_encoding)
376 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
376 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
377 if options[:description]
377 if options[:description]
378 if description = query.available_columns.detect {|q| q.name == :description}
378 if description = query.available_columns.detect {|q| q.name == :description}
379 columns << description
379 columns << description
380 end
380 end
381 end
381 end
382
382
383 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
383 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
384 # csv header fields
384 # csv header fields
385 csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
385 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
386 # csv lines
386 # csv lines
387 issues.each do |issue|
387 issues.each do |issue|
388 csv << [ issue.id.to_s ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(csv_content(c, issue), encoding) }
388 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(csv_content(c, issue), encoding) }
389 end
389 end
390 end
390 end
391 export
391 export
392 end
392 end
393 end
393 end
@@ -1,164 +1,166
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module QueriesHelper
20 module QueriesHelper
21 def filters_options_for_select(query)
21 def filters_options_for_select(query)
22 options_for_select(filters_options(query))
22 options_for_select(filters_options(query))
23 end
23 end
24
24
25 def filters_options(query)
25 def filters_options(query)
26 options = [[]]
26 options = [[]]
27 options += query.available_filters.map do |field, field_options|
27 options += query.available_filters.map do |field, field_options|
28 [field_options[:name], field]
28 [field_options[:name], field]
29 end
29 end
30 end
30 end
31
31
32 def available_block_columns_tags(query)
32 def available_block_columns_tags(query)
33 tags = ''.html_safe
33 tags = ''.html_safe
34 query.available_block_columns.each do |column|
34 query.available_block_columns.each do |column|
35 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
35 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
36 end
36 end
37 tags
37 tags
38 end
38 end
39
39
40 def column_header(column)
40 def column_header(column)
41 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
41 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
42 :default_order => column.default_order) :
42 :default_order => column.default_order) :
43 content_tag('th', h(column.caption))
43 content_tag('th', h(column.caption))
44 end
44 end
45
45
46 def column_content(column, issue)
46 def column_content(column, issue)
47 value = column.value(issue)
47 value = column.value(issue)
48 if value.is_a?(Array)
48 if value.is_a?(Array)
49 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
49 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
50 else
50 else
51 column_value(column, issue, value)
51 column_value(column, issue, value)
52 end
52 end
53 end
53 end
54
54
55 def column_value(column, issue, value)
55 def column_value(column, issue, value)
56 case value.class.name
56 case value.class.name
57 when 'String'
57 when 'String'
58 if column.name == :subject
58 if column.name == :subject
59 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
59 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
60 elsif column.name == :description
60 elsif column.name == :description
61 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
61 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
62 else
62 else
63 h(value)
63 h(value)
64 end
64 end
65 when 'Time'
65 when 'Time'
66 format_time(value)
66 format_time(value)
67 when 'Date'
67 when 'Date'
68 format_date(value)
68 format_date(value)
69 when 'Fixnum'
69 when 'Fixnum'
70 if column.name == :done_ratio
70 if column.name == :id
71 link_to value, issue_path(issue)
72 elsif column.name == :done_ratio
71 progress_bar(value, :width => '80px')
73 progress_bar(value, :width => '80px')
72 else
74 else
73 value.to_s
75 value.to_s
74 end
76 end
75 when 'Float'
77 when 'Float'
76 sprintf "%.2f", value
78 sprintf "%.2f", value
77 when 'User'
79 when 'User'
78 link_to_user value
80 link_to_user value
79 when 'Project'
81 when 'Project'
80 link_to_project value
82 link_to_project value
81 when 'Version'
83 when 'Version'
82 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
84 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
83 when 'TrueClass'
85 when 'TrueClass'
84 l(:general_text_Yes)
86 l(:general_text_Yes)
85 when 'FalseClass'
87 when 'FalseClass'
86 l(:general_text_No)
88 l(:general_text_No)
87 when 'Issue'
89 when 'Issue'
88 value.visible? ? link_to_issue(value) : "##{value.id}"
90 value.visible? ? link_to_issue(value) : "##{value.id}"
89 when 'IssueRelation'
91 when 'IssueRelation'
90 other = value.other_issue(issue)
92 other = value.other_issue(issue)
91 content_tag('span',
93 content_tag('span',
92 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
94 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
93 :class => value.css_classes_for(issue))
95 :class => value.css_classes_for(issue))
94 else
96 else
95 h(value)
97 h(value)
96 end
98 end
97 end
99 end
98
100
99 def csv_content(column, issue)
101 def csv_content(column, issue)
100 value = column.value(issue)
102 value = column.value(issue)
101 if value.is_a?(Array)
103 if value.is_a?(Array)
102 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
104 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
103 else
105 else
104 csv_value(column, issue, value)
106 csv_value(column, issue, value)
105 end
107 end
106 end
108 end
107
109
108 def csv_value(column, issue, value)
110 def csv_value(column, issue, value)
109 case value.class.name
111 case value.class.name
110 when 'Time'
112 when 'Time'
111 format_time(value)
113 format_time(value)
112 when 'Date'
114 when 'Date'
113 format_date(value)
115 format_date(value)
114 when 'Float'
116 when 'Float'
115 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
117 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
116 when 'IssueRelation'
118 when 'IssueRelation'
117 other = value.other_issue(issue)
119 other = value.other_issue(issue)
118 l(value.label_for(issue)) + " ##{other.id}"
120 l(value.label_for(issue)) + " ##{other.id}"
119 else
121 else
120 value.to_s
122 value.to_s
121 end
123 end
122 end
124 end
123
125
124 # Retrieve query from session or build a new query
126 # Retrieve query from session or build a new query
125 def retrieve_query
127 def retrieve_query
126 if !params[:query_id].blank?
128 if !params[:query_id].blank?
127 cond = "project_id IS NULL"
129 cond = "project_id IS NULL"
128 cond << " OR project_id = #{@project.id}" if @project
130 cond << " OR project_id = #{@project.id}" if @project
129 @query = IssueQuery.find(params[:query_id], :conditions => cond)
131 @query = IssueQuery.find(params[:query_id], :conditions => cond)
130 raise ::Unauthorized unless @query.visible?
132 raise ::Unauthorized unless @query.visible?
131 @query.project = @project
133 @query.project = @project
132 session[:query] = {:id => @query.id, :project_id => @query.project_id}
134 session[:query] = {:id => @query.id, :project_id => @query.project_id}
133 sort_clear
135 sort_clear
134 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
136 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
135 # Give it a name, required to be valid
137 # Give it a name, required to be valid
136 @query = IssueQuery.new(:name => "_")
138 @query = IssueQuery.new(:name => "_")
137 @query.project = @project
139 @query.project = @project
138 @query.build_from_params(params)
140 @query.build_from_params(params)
139 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
141 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
140 else
142 else
141 # retrieve from session
143 # retrieve from session
142 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
144 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
143 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
145 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
144 @query.project = @project
146 @query.project = @project
145 end
147 end
146 end
148 end
147
149
148 def retrieve_query_from_session
150 def retrieve_query_from_session
149 if session[:query]
151 if session[:query]
150 if session[:query][:id]
152 if session[:query][:id]
151 @query = IssueQuery.find_by_id(session[:query][:id])
153 @query = IssueQuery.find_by_id(session[:query][:id])
152 return unless @query
154 return unless @query
153 else
155 else
154 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
156 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
155 end
157 end
156 if session[:query].has_key?(:project_id)
158 if session[:query].has_key?(:project_id)
157 @query.project_id = session[:query][:project_id]
159 @query.project_id = session[:query][:project_id]
158 else
160 else
159 @query.project = @project
161 @query.project = @project
160 end
162 end
161 @query
163 @query
162 end
164 end
163 end
165 end
164 end
166 end
@@ -1,408 +1,405
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssueQuery < Query
18 class IssueQuery < Query
19
19
20 self.queried_class = Issue
20 self.queried_class = Issue
21
21
22 self.available_columns = [
22 self.available_columns = [
23 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
23 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
24 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
25 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
25 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
26 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
26 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
27 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
27 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
28 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
28 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
29 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
29 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
30 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
30 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
31 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
31 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
32 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
32 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
33 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
33 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
34 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
34 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
35 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
35 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
36 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
36 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
37 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
37 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
38 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
38 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
39 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
39 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
40 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
40 QueryColumn.new(:relations, :caption => :label_related_issues),
41 QueryColumn.new(:relations, :caption => :label_related_issues),
41 QueryColumn.new(:description, :inline => false)
42 QueryColumn.new(:description, :inline => false)
42 ]
43 ]
43
44
44 scope :visible, lambda {|*args|
45 scope :visible, lambda {|*args|
45 user = args.shift || User.current
46 user = args.shift || User.current
46 base = Project.allowed_to_condition(user, :view_issues, *args)
47 base = Project.allowed_to_condition(user, :view_issues, *args)
47 user_id = user.logged? ? user.id : 0
48 user_id = user.logged? ? user.id : 0
48
49
49 includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
50 includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
50 }
51 }
51
52
52 def initialize(attributes=nil, *args)
53 def initialize(attributes=nil, *args)
53 super attributes
54 super attributes
54 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
55 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
55 end
56 end
56
57
57 # Returns true if the query is visible to +user+ or the current user.
58 # Returns true if the query is visible to +user+ or the current user.
58 def visible?(user=User.current)
59 def visible?(user=User.current)
59 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
60 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
60 end
61 end
61
62
62 def initialize_available_filters
63 def initialize_available_filters
63 principals = []
64 principals = []
64 subprojects = []
65 subprojects = []
65 versions = []
66 versions = []
66 categories = []
67 categories = []
67 issue_custom_fields = []
68 issue_custom_fields = []
68
69
69 if project
70 if project
70 principals += project.principals.sort
71 principals += project.principals.sort
71 unless project.leaf?
72 unless project.leaf?
72 subprojects = project.descendants.visible.all
73 subprojects = project.descendants.visible.all
73 principals += Principal.member_of(subprojects)
74 principals += Principal.member_of(subprojects)
74 end
75 end
75 versions = project.shared_versions.all
76 versions = project.shared_versions.all
76 categories = project.issue_categories.all
77 categories = project.issue_categories.all
77 issue_custom_fields = project.all_issue_custom_fields
78 issue_custom_fields = project.all_issue_custom_fields
78 else
79 else
79 if all_projects.any?
80 if all_projects.any?
80 principals += Principal.member_of(all_projects)
81 principals += Principal.member_of(all_projects)
81 end
82 end
82 versions = Version.visible.find_all_by_sharing('system')
83 versions = Version.visible.find_all_by_sharing('system')
83 issue_custom_fields = IssueCustomField.where(:is_filter => true, :is_for_all => true).all
84 issue_custom_fields = IssueCustomField.where(:is_filter => true, :is_for_all => true).all
84 end
85 end
85 principals.uniq!
86 principals.uniq!
86 principals.sort!
87 principals.sort!
87 users = principals.select {|p| p.is_a?(User)}
88 users = principals.select {|p| p.is_a?(User)}
88
89
89
90
90 add_available_filter "status_id",
91 add_available_filter "status_id",
91 :type => :list_status, :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
92 :type => :list_status, :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
92
93
93 if project.nil?
94 if project.nil?
94 project_values = []
95 project_values = []
95 if User.current.logged? && User.current.memberships.any?
96 if User.current.logged? && User.current.memberships.any?
96 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
97 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
97 end
98 end
98 project_values += all_projects_values
99 project_values += all_projects_values
99 add_available_filter("project_id",
100 add_available_filter("project_id",
100 :type => :list, :values => project_values
101 :type => :list, :values => project_values
101 ) unless project_values.empty?
102 ) unless project_values.empty?
102 end
103 end
103
104
104 add_available_filter "tracker_id",
105 add_available_filter "tracker_id",
105 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
106 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
106 add_available_filter "priority_id",
107 add_available_filter "priority_id",
107 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
108 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
108
109
109 author_values = []
110 author_values = []
110 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
111 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
111 author_values += users.collect{|s| [s.name, s.id.to_s] }
112 author_values += users.collect{|s| [s.name, s.id.to_s] }
112 add_available_filter("author_id",
113 add_available_filter("author_id",
113 :type => :list, :values => author_values
114 :type => :list, :values => author_values
114 ) unless author_values.empty?
115 ) unless author_values.empty?
115
116
116 assigned_to_values = []
117 assigned_to_values = []
117 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
118 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
118 assigned_to_values += (Setting.issue_group_assignment? ?
119 assigned_to_values += (Setting.issue_group_assignment? ?
119 principals : users).collect{|s| [s.name, s.id.to_s] }
120 principals : users).collect{|s| [s.name, s.id.to_s] }
120 add_available_filter("assigned_to_id",
121 add_available_filter("assigned_to_id",
121 :type => :list_optional, :values => assigned_to_values
122 :type => :list_optional, :values => assigned_to_values
122 ) unless assigned_to_values.empty?
123 ) unless assigned_to_values.empty?
123
124
124 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
125 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
125 add_available_filter("member_of_group",
126 add_available_filter("member_of_group",
126 :type => :list_optional, :values => group_values
127 :type => :list_optional, :values => group_values
127 ) unless group_values.empty?
128 ) unless group_values.empty?
128
129
129 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
130 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
130 add_available_filter("assigned_to_role",
131 add_available_filter("assigned_to_role",
131 :type => :list_optional, :values => role_values
132 :type => :list_optional, :values => role_values
132 ) unless role_values.empty?
133 ) unless role_values.empty?
133
134
134 if versions.any?
135 if versions.any?
135 add_available_filter "fixed_version_id",
136 add_available_filter "fixed_version_id",
136 :type => :list_optional,
137 :type => :list_optional,
137 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
138 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
138 end
139 end
139
140
140 if categories.any?
141 if categories.any?
141 add_available_filter "category_id",
142 add_available_filter "category_id",
142 :type => :list_optional,
143 :type => :list_optional,
143 :values => categories.collect{|s| [s.name, s.id.to_s] }
144 :values => categories.collect{|s| [s.name, s.id.to_s] }
144 end
145 end
145
146
146 add_available_filter "subject", :type => :text
147 add_available_filter "subject", :type => :text
147 add_available_filter "created_on", :type => :date_past
148 add_available_filter "created_on", :type => :date_past
148 add_available_filter "updated_on", :type => :date_past
149 add_available_filter "updated_on", :type => :date_past
149 add_available_filter "closed_on", :type => :date_past
150 add_available_filter "closed_on", :type => :date_past
150 add_available_filter "start_date", :type => :date
151 add_available_filter "start_date", :type => :date
151 add_available_filter "due_date", :type => :date
152 add_available_filter "due_date", :type => :date
152 add_available_filter "estimated_hours", :type => :float
153 add_available_filter "estimated_hours", :type => :float
153 add_available_filter "done_ratio", :type => :integer
154 add_available_filter "done_ratio", :type => :integer
154
155
155 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
156 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
156 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
157 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
157 add_available_filter "is_private",
158 add_available_filter "is_private",
158 :type => :list,
159 :type => :list,
159 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
160 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
160 end
161 end
161
162
162 if User.current.logged?
163 if User.current.logged?
163 add_available_filter "watcher_id",
164 add_available_filter "watcher_id",
164 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
165 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
165 end
166 end
166
167
167 if subprojects.any?
168 if subprojects.any?
168 add_available_filter "subproject_id",
169 add_available_filter "subproject_id",
169 :type => :list_subprojects,
170 :type => :list_subprojects,
170 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
171 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
171 end
172 end
172
173
173 add_custom_fields_filters(issue_custom_fields)
174 add_custom_fields_filters(issue_custom_fields)
174
175
175 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
176 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
176
177
177 IssueRelation::TYPES.each do |relation_type, options|
178 IssueRelation::TYPES.each do |relation_type, options|
178 add_available_filter relation_type, :type => :relation, :label => options[:name]
179 add_available_filter relation_type, :type => :relation, :label => options[:name]
179 end
180 end
180
181
181 Tracker.disabled_core_fields(trackers).each {|field|
182 Tracker.disabled_core_fields(trackers).each {|field|
182 delete_available_filter field
183 delete_available_filter field
183 }
184 }
184 end
185 end
185
186
186 def available_columns
187 def available_columns
187 return @available_columns if @available_columns
188 return @available_columns if @available_columns
188 @available_columns = self.class.available_columns.dup
189 @available_columns = self.class.available_columns.dup
189 @available_columns += (project ?
190 @available_columns += (project ?
190 project.all_issue_custom_fields :
191 project.all_issue_custom_fields :
191 IssueCustomField.all
192 IssueCustomField.all
192 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
193 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
193
194
194 if User.current.allowed_to?(:view_time_entries, project, :global => true)
195 if User.current.allowed_to?(:view_time_entries, project, :global => true)
195 index = nil
196 index = nil
196 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
197 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
197 index = (index ? index + 1 : -1)
198 index = (index ? index + 1 : -1)
198 # insert the column after estimated_hours or at the end
199 # insert the column after estimated_hours or at the end
199 @available_columns.insert index, QueryColumn.new(:spent_hours,
200 @available_columns.insert index, QueryColumn.new(:spent_hours,
200 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
201 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
201 :default_order => 'desc',
202 :default_order => 'desc',
202 :caption => :label_spent_time
203 :caption => :label_spent_time
203 )
204 )
204 end
205 end
205
206
206 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
207 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
207 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
208 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
208 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
209 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
209 end
210 end
210
211
211 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
212 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
212 @available_columns.reject! {|column|
213 @available_columns.reject! {|column|
213 disabled_fields.include?(column.name.to_s)
214 disabled_fields.include?(column.name.to_s)
214 }
215 }
215
216
216 @available_columns
217 @available_columns
217 end
218 end
218
219
219 def sortable_columns
220 {'id' => "#{Issue.table_name}.id"}.merge(super)
221 end
222
223 def default_columns_names
220 def default_columns_names
224 @default_columns_names ||= begin
221 @default_columns_names ||= begin
225 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
222 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
226
223
227 project.present? ? default_columns : [:project] | default_columns
224 project.present? ? default_columns : [:project] | default_columns
228 end
225 end
229 end
226 end
230
227
231 # Returns the issue count
228 # Returns the issue count
232 def issue_count
229 def issue_count
233 Issue.visible.count(:include => [:status, :project], :conditions => statement)
230 Issue.visible.count(:include => [:status, :project], :conditions => statement)
234 rescue ::ActiveRecord::StatementInvalid => e
231 rescue ::ActiveRecord::StatementInvalid => e
235 raise StatementInvalid.new(e.message)
232 raise StatementInvalid.new(e.message)
236 end
233 end
237
234
238 # Returns the issue count by group or nil if query is not grouped
235 # Returns the issue count by group or nil if query is not grouped
239 def issue_count_by_group
236 def issue_count_by_group
240 r = nil
237 r = nil
241 if grouped?
238 if grouped?
242 begin
239 begin
243 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
240 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
244 r = Issue.visible.count(:joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement)
241 r = Issue.visible.count(:joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement)
245 rescue ActiveRecord::RecordNotFound
242 rescue ActiveRecord::RecordNotFound
246 r = {nil => issue_count}
243 r = {nil => issue_count}
247 end
244 end
248 c = group_by_column
245 c = group_by_column
249 if c.is_a?(QueryCustomFieldColumn)
246 if c.is_a?(QueryCustomFieldColumn)
250 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
247 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
251 end
248 end
252 end
249 end
253 r
250 r
254 rescue ::ActiveRecord::StatementInvalid => e
251 rescue ::ActiveRecord::StatementInvalid => e
255 raise StatementInvalid.new(e.message)
252 raise StatementInvalid.new(e.message)
256 end
253 end
257
254
258 # Returns the issues
255 # Returns the issues
259 # Valid options are :order, :offset, :limit, :include, :conditions
256 # Valid options are :order, :offset, :limit, :include, :conditions
260 def issues(options={})
257 def issues(options={})
261 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
258 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
262
259
263 issues = Issue.visible.where(options[:conditions]).all(
260 issues = Issue.visible.where(options[:conditions]).all(
264 :include => ([:status, :project] + (options[:include] || [])).uniq,
261 :include => ([:status, :project] + (options[:include] || [])).uniq,
265 :conditions => statement,
262 :conditions => statement,
266 :order => order_option,
263 :order => order_option,
267 :joins => joins_for_order_statement(order_option.join(',')),
264 :joins => joins_for_order_statement(order_option.join(',')),
268 :limit => options[:limit],
265 :limit => options[:limit],
269 :offset => options[:offset]
266 :offset => options[:offset]
270 )
267 )
271
268
272 if has_column?(:spent_hours)
269 if has_column?(:spent_hours)
273 Issue.load_visible_spent_hours(issues)
270 Issue.load_visible_spent_hours(issues)
274 end
271 end
275 if has_column?(:relations)
272 if has_column?(:relations)
276 Issue.load_visible_relations(issues)
273 Issue.load_visible_relations(issues)
277 end
274 end
278 issues
275 issues
279 rescue ::ActiveRecord::StatementInvalid => e
276 rescue ::ActiveRecord::StatementInvalid => e
280 raise StatementInvalid.new(e.message)
277 raise StatementInvalid.new(e.message)
281 end
278 end
282
279
283 # Returns the issues ids
280 # Returns the issues ids
284 def issue_ids(options={})
281 def issue_ids(options={})
285 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
282 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
286
283
287 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
284 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
288 :conditions => statement,
285 :conditions => statement,
289 :order => order_option,
286 :order => order_option,
290 :joins => joins_for_order_statement(order_option.join(',')),
287 :joins => joins_for_order_statement(order_option.join(',')),
291 :limit => options[:limit],
288 :limit => options[:limit],
292 :offset => options[:offset]).find_ids
289 :offset => options[:offset]).find_ids
293 rescue ::ActiveRecord::StatementInvalid => e
290 rescue ::ActiveRecord::StatementInvalid => e
294 raise StatementInvalid.new(e.message)
291 raise StatementInvalid.new(e.message)
295 end
292 end
296
293
297 # Returns the journals
294 # Returns the journals
298 # Valid options are :order, :offset, :limit
295 # Valid options are :order, :offset, :limit
299 def journals(options={})
296 def journals(options={})
300 Journal.visible.all(
297 Journal.visible.all(
301 :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
298 :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
302 :conditions => statement,
299 :conditions => statement,
303 :order => options[:order],
300 :order => options[:order],
304 :limit => options[:limit],
301 :limit => options[:limit],
305 :offset => options[:offset]
302 :offset => options[:offset]
306 )
303 )
307 rescue ::ActiveRecord::StatementInvalid => e
304 rescue ::ActiveRecord::StatementInvalid => e
308 raise StatementInvalid.new(e.message)
305 raise StatementInvalid.new(e.message)
309 end
306 end
310
307
311 # Returns the versions
308 # Returns the versions
312 # Valid options are :conditions
309 # Valid options are :conditions
313 def versions(options={})
310 def versions(options={})
314 Version.visible.where(options[:conditions]).all(
311 Version.visible.where(options[:conditions]).all(
315 :include => :project,
312 :include => :project,
316 :conditions => project_statement
313 :conditions => project_statement
317 )
314 )
318 rescue ::ActiveRecord::StatementInvalid => e
315 rescue ::ActiveRecord::StatementInvalid => e
319 raise StatementInvalid.new(e.message)
316 raise StatementInvalid.new(e.message)
320 end
317 end
321
318
322 def sql_for_watcher_id_field(field, operator, value)
319 def sql_for_watcher_id_field(field, operator, value)
323 db_table = Watcher.table_name
320 db_table = Watcher.table_name
324 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
321 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
325 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
322 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
326 end
323 end
327
324
328 def sql_for_member_of_group_field(field, operator, value)
325 def sql_for_member_of_group_field(field, operator, value)
329 if operator == '*' # Any group
326 if operator == '*' # Any group
330 groups = Group.all
327 groups = Group.all
331 operator = '=' # Override the operator since we want to find by assigned_to
328 operator = '=' # Override the operator since we want to find by assigned_to
332 elsif operator == "!*"
329 elsif operator == "!*"
333 groups = Group.all
330 groups = Group.all
334 operator = '!' # Override the operator since we want to find by assigned_to
331 operator = '!' # Override the operator since we want to find by assigned_to
335 else
332 else
336 groups = Group.find_all_by_id(value)
333 groups = Group.find_all_by_id(value)
337 end
334 end
338 groups ||= []
335 groups ||= []
339
336
340 members_of_groups = groups.inject([]) {|user_ids, group|
337 members_of_groups = groups.inject([]) {|user_ids, group|
341 user_ids + group.user_ids + [group.id]
338 user_ids + group.user_ids + [group.id]
342 }.uniq.compact.sort.collect(&:to_s)
339 }.uniq.compact.sort.collect(&:to_s)
343
340
344 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
341 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
345 end
342 end
346
343
347 def sql_for_assigned_to_role_field(field, operator, value)
344 def sql_for_assigned_to_role_field(field, operator, value)
348 case operator
345 case operator
349 when "*", "!*" # Member / Not member
346 when "*", "!*" # Member / Not member
350 sw = operator == "!*" ? 'NOT' : ''
347 sw = operator == "!*" ? 'NOT' : ''
351 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
348 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
352 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
349 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
353 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
350 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
354 when "=", "!"
351 when "=", "!"
355 role_cond = value.any? ?
352 role_cond = value.any? ?
356 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
353 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
357 "1=0"
354 "1=0"
358
355
359 sw = operator == "!" ? 'NOT' : ''
356 sw = operator == "!" ? 'NOT' : ''
360 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
357 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
361 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
358 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
362 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
359 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
363 end
360 end
364 end
361 end
365
362
366 def sql_for_is_private_field(field, operator, value)
363 def sql_for_is_private_field(field, operator, value)
367 op = (operator == "=" ? 'IN' : 'NOT IN')
364 op = (operator == "=" ? 'IN' : 'NOT IN')
368 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
365 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
369
366
370 "#{Issue.table_name}.is_private #{op} (#{va})"
367 "#{Issue.table_name}.is_private #{op} (#{va})"
371 end
368 end
372
369
373 def sql_for_relations(field, operator, value, options={})
370 def sql_for_relations(field, operator, value, options={})
374 relation_options = IssueRelation::TYPES[field]
371 relation_options = IssueRelation::TYPES[field]
375 return relation_options unless relation_options
372 return relation_options unless relation_options
376
373
377 relation_type = field
374 relation_type = field
378 join_column, target_join_column = "issue_from_id", "issue_to_id"
375 join_column, target_join_column = "issue_from_id", "issue_to_id"
379 if relation_options[:reverse] || options[:reverse]
376 if relation_options[:reverse] || options[:reverse]
380 relation_type = relation_options[:reverse] || relation_type
377 relation_type = relation_options[:reverse] || relation_type
381 join_column, target_join_column = target_join_column, join_column
378 join_column, target_join_column = target_join_column, join_column
382 end
379 end
383
380
384 sql = case operator
381 sql = case operator
385 when "*", "!*"
382 when "*", "!*"
386 op = (operator == "*" ? 'IN' : 'NOT IN')
383 op = (operator == "*" ? 'IN' : 'NOT IN')
387 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
384 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
388 when "=", "!"
385 when "=", "!"
389 op = (operator == "=" ? 'IN' : 'NOT IN')
386 op = (operator == "=" ? 'IN' : 'NOT IN')
390 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
387 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
391 when "=p", "=!p", "!p"
388 when "=p", "=!p", "!p"
392 op = (operator == "!p" ? 'NOT IN' : 'IN')
389 op = (operator == "!p" ? 'NOT IN' : 'IN')
393 comp = (operator == "=!p" ? '<>' : '=')
390 comp = (operator == "=!p" ? '<>' : '=')
394 "#{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 = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
391 "#{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 = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
395 end
392 end
396
393
397 if relation_options[:sym] == field && !options[:reverse]
394 if relation_options[:sym] == field && !options[:reverse]
398 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
395 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
399 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
396 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
400 else
397 else
401 sql
398 sql
402 end
399 end
403 end
400 end
404
401
405 IssueRelation::TYPES.keys.each do |relation_type|
402 IssueRelation::TYPES.keys.each do |relation_type|
406 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
403 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
407 end
404 end
408 end
405 end
@@ -1,822 +1,828
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @inline = options.key?(:inline) ? options[:inline] : true
30 @inline = options.key?(:inline) ? options[:inline] : true
31 @caption_key = options[:caption] || "field_#{name}"
31 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @frozen = options[:frozen]
32 end
33 end
33
34
34 def caption
35 def caption
35 l(@caption_key)
36 @caption_key.is_a?(Symbol) ? l(@caption_key) : @caption_key
36 end
37 end
37
38
38 # Returns true if the column is sortable, otherwise false
39 # Returns true if the column is sortable, otherwise false
39 def sortable?
40 def sortable?
40 !@sortable.nil?
41 !@sortable.nil?
41 end
42 end
42
43
43 def sortable
44 def sortable
44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
45 @sortable.is_a?(Proc) ? @sortable.call : @sortable
45 end
46 end
46
47
47 def inline?
48 def inline?
48 @inline
49 @inline
49 end
50 end
50
51
52 def frozen?
53 @frozen
54 end
55
51 def value(object)
56 def value(object)
52 object.send name
57 object.send name
53 end
58 end
54
59
55 def css_classes
60 def css_classes
56 name
61 name
57 end
62 end
58 end
63 end
59
64
60 class QueryCustomFieldColumn < QueryColumn
65 class QueryCustomFieldColumn < QueryColumn
61
66
62 def initialize(custom_field)
67 def initialize(custom_field)
63 self.name = "cf_#{custom_field.id}".to_sym
68 self.name = "cf_#{custom_field.id}".to_sym
64 self.sortable = custom_field.order_statement || false
69 self.sortable = custom_field.order_statement || false
65 self.groupable = custom_field.group_statement || false
70 self.groupable = custom_field.group_statement || false
66 @inline = true
71 @inline = true
67 @cf = custom_field
72 @cf = custom_field
68 end
73 end
69
74
70 def caption
75 def caption
71 @cf.name
76 @cf.name
72 end
77 end
73
78
74 def custom_field
79 def custom_field
75 @cf
80 @cf
76 end
81 end
77
82
78 def value(object)
83 def value(object)
79 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
84 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
85 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
81 end
86 end
82
87
83 def css_classes
88 def css_classes
84 @css_classes ||= "#{name} #{@cf.field_format}"
89 @css_classes ||= "#{name} #{@cf.field_format}"
85 end
90 end
86 end
91 end
87
92
88 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
93 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
89
94
90 def initialize(association, custom_field)
95 def initialize(association, custom_field)
91 super(custom_field)
96 super(custom_field)
92 self.name = "#{association}.cf_#{custom_field.id}".to_sym
97 self.name = "#{association}.cf_#{custom_field.id}".to_sym
93 # TODO: support sorting/grouping by association custom field
98 # TODO: support sorting/grouping by association custom field
94 self.sortable = false
99 self.sortable = false
95 self.groupable = false
100 self.groupable = false
96 @association = association
101 @association = association
97 end
102 end
98
103
99 def value(object)
104 def value(object)
100 if assoc = object.send(@association)
105 if assoc = object.send(@association)
101 super(assoc)
106 super(assoc)
102 end
107 end
103 end
108 end
104
109
105 def css_classes
110 def css_classes
106 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
111 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
107 end
112 end
108 end
113 end
109
114
110 class Query < ActiveRecord::Base
115 class Query < ActiveRecord::Base
111 class StatementInvalid < ::ActiveRecord::StatementInvalid
116 class StatementInvalid < ::ActiveRecord::StatementInvalid
112 end
117 end
113
118
114 belongs_to :project
119 belongs_to :project
115 belongs_to :user
120 belongs_to :user
116 serialize :filters
121 serialize :filters
117 serialize :column_names
122 serialize :column_names
118 serialize :sort_criteria, Array
123 serialize :sort_criteria, Array
119
124
120 attr_protected :project_id, :user_id
125 attr_protected :project_id, :user_id
121
126
122 validates_presence_of :name
127 validates_presence_of :name
123 validates_length_of :name, :maximum => 255
128 validates_length_of :name, :maximum => 255
124 validate :validate_query_filters
129 validate :validate_query_filters
125
130
126 class_attribute :operators
131 class_attribute :operators
127 self.operators = {
132 self.operators = {
128 "=" => :label_equals,
133 "=" => :label_equals,
129 "!" => :label_not_equals,
134 "!" => :label_not_equals,
130 "o" => :label_open_issues,
135 "o" => :label_open_issues,
131 "c" => :label_closed_issues,
136 "c" => :label_closed_issues,
132 "!*" => :label_none,
137 "!*" => :label_none,
133 "*" => :label_any,
138 "*" => :label_any,
134 ">=" => :label_greater_or_equal,
139 ">=" => :label_greater_or_equal,
135 "<=" => :label_less_or_equal,
140 "<=" => :label_less_or_equal,
136 "><" => :label_between,
141 "><" => :label_between,
137 "<t+" => :label_in_less_than,
142 "<t+" => :label_in_less_than,
138 ">t+" => :label_in_more_than,
143 ">t+" => :label_in_more_than,
139 "><t+"=> :label_in_the_next_days,
144 "><t+"=> :label_in_the_next_days,
140 "t+" => :label_in,
145 "t+" => :label_in,
141 "t" => :label_today,
146 "t" => :label_today,
142 "ld" => :label_yesterday,
147 "ld" => :label_yesterday,
143 "w" => :label_this_week,
148 "w" => :label_this_week,
144 "lw" => :label_last_week,
149 "lw" => :label_last_week,
145 "l2w" => [:label_last_n_weeks, {:count => 2}],
150 "l2w" => [:label_last_n_weeks, {:count => 2}],
146 "m" => :label_this_month,
151 "m" => :label_this_month,
147 "lm" => :label_last_month,
152 "lm" => :label_last_month,
148 "y" => :label_this_year,
153 "y" => :label_this_year,
149 ">t-" => :label_less_than_ago,
154 ">t-" => :label_less_than_ago,
150 "<t-" => :label_more_than_ago,
155 "<t-" => :label_more_than_ago,
151 "><t-"=> :label_in_the_past_days,
156 "><t-"=> :label_in_the_past_days,
152 "t-" => :label_ago,
157 "t-" => :label_ago,
153 "~" => :label_contains,
158 "~" => :label_contains,
154 "!~" => :label_not_contains,
159 "!~" => :label_not_contains,
155 "=p" => :label_any_issues_in_project,
160 "=p" => :label_any_issues_in_project,
156 "=!p" => :label_any_issues_not_in_project,
161 "=!p" => :label_any_issues_not_in_project,
157 "!p" => :label_no_issues_in_project
162 "!p" => :label_no_issues_in_project
158 }
163 }
159
164
160 class_attribute :operators_by_filter_type
165 class_attribute :operators_by_filter_type
161 self.operators_by_filter_type = {
166 self.operators_by_filter_type = {
162 :list => [ "=", "!" ],
167 :list => [ "=", "!" ],
163 :list_status => [ "o", "=", "!", "c", "*" ],
168 :list_status => [ "o", "=", "!", "c", "*" ],
164 :list_optional => [ "=", "!", "!*", "*" ],
169 :list_optional => [ "=", "!", "!*", "*" ],
165 :list_subprojects => [ "*", "!*", "=" ],
170 :list_subprojects => [ "*", "!*", "=" ],
166 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
171 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
167 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
172 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
168 :string => [ "=", "~", "!", "!~", "!*", "*" ],
173 :string => [ "=", "~", "!", "!~", "!*", "*" ],
169 :text => [ "~", "!~", "!*", "*" ],
174 :text => [ "~", "!~", "!*", "*" ],
170 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
175 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
171 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
176 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
172 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
177 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
173 }
178 }
174
179
175 class_attribute :available_columns
180 class_attribute :available_columns
176 self.available_columns = []
181 self.available_columns = []
177
182
178 class_attribute :queried_class
183 class_attribute :queried_class
179
184
180 def queried_table_name
185 def queried_table_name
181 @queried_table_name ||= self.class.queried_class.table_name
186 @queried_table_name ||= self.class.queried_class.table_name
182 end
187 end
183
188
184 def initialize(attributes=nil, *args)
189 def initialize(attributes=nil, *args)
185 super attributes
190 super attributes
186 @is_for_all = project.nil?
191 @is_for_all = project.nil?
187 end
192 end
188
193
189 # Builds the query from the given params
194 # Builds the query from the given params
190 def build_from_params(params)
195 def build_from_params(params)
191 if params[:fields] || params[:f]
196 if params[:fields] || params[:f]
192 self.filters = {}
197 self.filters = {}
193 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
198 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
194 else
199 else
195 available_filters.keys.each do |field|
200 available_filters.keys.each do |field|
196 add_short_filter(field, params[field]) if params[field]
201 add_short_filter(field, params[field]) if params[field]
197 end
202 end
198 end
203 end
199 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
204 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
200 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
205 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
201 self
206 self
202 end
207 end
203
208
204 # Builds a new query from the given params and attributes
209 # Builds a new query from the given params and attributes
205 def self.build_from_params(params, attributes={})
210 def self.build_from_params(params, attributes={})
206 new(attributes).build_from_params(params)
211 new(attributes).build_from_params(params)
207 end
212 end
208
213
209 def validate_query_filters
214 def validate_query_filters
210 filters.each_key do |field|
215 filters.each_key do |field|
211 if values_for(field)
216 if values_for(field)
212 case type_for(field)
217 case type_for(field)
213 when :integer
218 when :integer
214 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
219 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
215 when :float
220 when :float
216 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
221 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
217 when :date, :date_past
222 when :date, :date_past
218 case operator_for(field)
223 case operator_for(field)
219 when "=", ">=", "<=", "><"
224 when "=", ">=", "<=", "><"
220 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
225 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
221 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
226 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
222 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
227 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
223 end
228 end
224 end
229 end
225 end
230 end
226
231
227 add_filter_error(field, :blank) unless
232 add_filter_error(field, :blank) unless
228 # filter requires one or more values
233 # filter requires one or more values
229 (values_for(field) and !values_for(field).first.blank?) or
234 (values_for(field) and !values_for(field).first.blank?) or
230 # filter doesn't require any value
235 # filter doesn't require any value
231 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
236 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
232 end if filters
237 end if filters
233 end
238 end
234
239
235 def add_filter_error(field, message)
240 def add_filter_error(field, message)
236 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
241 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
237 errors.add(:base, m)
242 errors.add(:base, m)
238 end
243 end
239
244
240 def editable_by?(user)
245 def editable_by?(user)
241 return false unless user
246 return false unless user
242 # Admin can edit them all and regular users can edit their private queries
247 # Admin can edit them all and regular users can edit their private queries
243 return true if user.admin? || (!is_public && self.user_id == user.id)
248 return true if user.admin? || (!is_public && self.user_id == user.id)
244 # Members can not edit public queries that are for all project (only admin is allowed to)
249 # Members can not edit public queries that are for all project (only admin is allowed to)
245 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
250 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
246 end
251 end
247
252
248 def trackers
253 def trackers
249 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
254 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
250 end
255 end
251
256
252 # Returns a hash of localized labels for all filter operators
257 # Returns a hash of localized labels for all filter operators
253 def self.operators_labels
258 def self.operators_labels
254 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
259 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
255 end
260 end
256
261
257 # Returns a representation of the available filters for JSON serialization
262 # Returns a representation of the available filters for JSON serialization
258 def available_filters_as_json
263 def available_filters_as_json
259 json = {}
264 json = {}
260 available_filters.each do |field, options|
265 available_filters.each do |field, options|
261 json[field] = options.slice(:type, :name, :values).stringify_keys
266 json[field] = options.slice(:type, :name, :values).stringify_keys
262 end
267 end
263 json
268 json
264 end
269 end
265
270
266 def all_projects
271 def all_projects
267 @all_projects ||= Project.visible.all
272 @all_projects ||= Project.visible.all
268 end
273 end
269
274
270 def all_projects_values
275 def all_projects_values
271 return @all_projects_values if @all_projects_values
276 return @all_projects_values if @all_projects_values
272
277
273 values = []
278 values = []
274 Project.project_tree(all_projects) do |p, level|
279 Project.project_tree(all_projects) do |p, level|
275 prefix = (level > 0 ? ('--' * level + ' ') : '')
280 prefix = (level > 0 ? ('--' * level + ' ') : '')
276 values << ["#{prefix}#{p.name}", p.id.to_s]
281 values << ["#{prefix}#{p.name}", p.id.to_s]
277 end
282 end
278 @all_projects_values = values
283 @all_projects_values = values
279 end
284 end
280
285
281 # Adds available filters
286 # Adds available filters
282 def initialize_available_filters
287 def initialize_available_filters
283 # implemented by sub-classes
288 # implemented by sub-classes
284 end
289 end
285 protected :initialize_available_filters
290 protected :initialize_available_filters
286
291
287 # Adds an available filter
292 # Adds an available filter
288 def add_available_filter(field, options)
293 def add_available_filter(field, options)
289 @available_filters ||= ActiveSupport::OrderedHash.new
294 @available_filters ||= ActiveSupport::OrderedHash.new
290 @available_filters[field] = options
295 @available_filters[field] = options
291 @available_filters
296 @available_filters
292 end
297 end
293
298
294 # Removes an available filter
299 # Removes an available filter
295 def delete_available_filter(field)
300 def delete_available_filter(field)
296 if @available_filters
301 if @available_filters
297 @available_filters.delete(field)
302 @available_filters.delete(field)
298 end
303 end
299 end
304 end
300
305
301 # Return a hash of available filters
306 # Return a hash of available filters
302 def available_filters
307 def available_filters
303 unless @available_filters
308 unless @available_filters
304 initialize_available_filters
309 initialize_available_filters
305 @available_filters.each do |field, options|
310 @available_filters.each do |field, options|
306 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
311 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
307 end
312 end
308 end
313 end
309 @available_filters
314 @available_filters
310 end
315 end
311
316
312 def add_filter(field, operator, values=nil)
317 def add_filter(field, operator, values=nil)
313 # values must be an array
318 # values must be an array
314 return unless values.nil? || values.is_a?(Array)
319 return unless values.nil? || values.is_a?(Array)
315 # check if field is defined as an available filter
320 # check if field is defined as an available filter
316 if available_filters.has_key? field
321 if available_filters.has_key? field
317 filter_options = available_filters[field]
322 filter_options = available_filters[field]
318 filters[field] = {:operator => operator, :values => (values || [''])}
323 filters[field] = {:operator => operator, :values => (values || [''])}
319 end
324 end
320 end
325 end
321
326
322 def add_short_filter(field, expression)
327 def add_short_filter(field, expression)
323 return unless expression && available_filters.has_key?(field)
328 return unless expression && available_filters.has_key?(field)
324 field_type = available_filters[field][:type]
329 field_type = available_filters[field][:type]
325 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
330 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
326 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
331 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
327 values = $1
332 values = $1
328 add_filter field, operator, values.present? ? values.split('|') : ['']
333 add_filter field, operator, values.present? ? values.split('|') : ['']
329 end || add_filter(field, '=', expression.split('|'))
334 end || add_filter(field, '=', expression.split('|'))
330 end
335 end
331
336
332 # Add multiple filters using +add_filter+
337 # Add multiple filters using +add_filter+
333 def add_filters(fields, operators, values)
338 def add_filters(fields, operators, values)
334 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
339 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
335 fields.each do |field|
340 fields.each do |field|
336 add_filter(field, operators[field], values && values[field])
341 add_filter(field, operators[field], values && values[field])
337 end
342 end
338 end
343 end
339 end
344 end
340
345
341 def has_filter?(field)
346 def has_filter?(field)
342 filters and filters[field]
347 filters and filters[field]
343 end
348 end
344
349
345 def type_for(field)
350 def type_for(field)
346 available_filters[field][:type] if available_filters.has_key?(field)
351 available_filters[field][:type] if available_filters.has_key?(field)
347 end
352 end
348
353
349 def operator_for(field)
354 def operator_for(field)
350 has_filter?(field) ? filters[field][:operator] : nil
355 has_filter?(field) ? filters[field][:operator] : nil
351 end
356 end
352
357
353 def values_for(field)
358 def values_for(field)
354 has_filter?(field) ? filters[field][:values] : nil
359 has_filter?(field) ? filters[field][:values] : nil
355 end
360 end
356
361
357 def value_for(field, index=0)
362 def value_for(field, index=0)
358 (values_for(field) || [])[index]
363 (values_for(field) || [])[index]
359 end
364 end
360
365
361 def label_for(field)
366 def label_for(field)
362 label = available_filters[field][:name] if available_filters.has_key?(field)
367 label = available_filters[field][:name] if available_filters.has_key?(field)
363 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
368 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
364 end
369 end
365
370
366 def self.add_available_column(column)
371 def self.add_available_column(column)
367 self.available_columns << (column) if column.is_a?(QueryColumn)
372 self.available_columns << (column) if column.is_a?(QueryColumn)
368 end
373 end
369
374
370 # Returns an array of columns that can be used to group the results
375 # Returns an array of columns that can be used to group the results
371 def groupable_columns
376 def groupable_columns
372 available_columns.select {|c| c.groupable}
377 available_columns.select {|c| c.groupable}
373 end
378 end
374
379
375 # Returns a Hash of columns and the key for sorting
380 # Returns a Hash of columns and the key for sorting
376 def sortable_columns
381 def sortable_columns
377 available_columns.inject({}) {|h, column|
382 available_columns.inject({}) {|h, column|
378 h[column.name.to_s] = column.sortable
383 h[column.name.to_s] = column.sortable
379 h
384 h
380 }
385 }
381 end
386 end
382
387
383 def columns
388 def columns
384 # preserve the column_names order
389 # preserve the column_names order
385 (has_default_columns? ? default_columns_names : column_names).collect do |name|
390 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
386 available_columns.find { |col| col.name == name }
391 available_columns.find { |col| col.name == name }
387 end.compact
392 end.compact
393 available_columns.select(&:frozen?) | cols
388 end
394 end
389
395
390 def inline_columns
396 def inline_columns
391 columns.select(&:inline?)
397 columns.select(&:inline?)
392 end
398 end
393
399
394 def block_columns
400 def block_columns
395 columns.reject(&:inline?)
401 columns.reject(&:inline?)
396 end
402 end
397
403
398 def available_inline_columns
404 def available_inline_columns
399 available_columns.select(&:inline?)
405 available_columns.select(&:inline?)
400 end
406 end
401
407
402 def available_block_columns
408 def available_block_columns
403 available_columns.reject(&:inline?)
409 available_columns.reject(&:inline?)
404 end
410 end
405
411
406 def default_columns_names
412 def default_columns_names
407 []
413 []
408 end
414 end
409
415
410 def column_names=(names)
416 def column_names=(names)
411 if names
417 if names
412 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
418 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
413 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
419 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
414 # Set column_names to nil if default columns
420 # Set column_names to nil if default columns
415 if names == default_columns_names
421 if names == default_columns_names
416 names = nil
422 names = nil
417 end
423 end
418 end
424 end
419 write_attribute(:column_names, names)
425 write_attribute(:column_names, names)
420 end
426 end
421
427
422 def has_column?(column)
428 def has_column?(column)
423 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
429 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
424 end
430 end
425
431
426 def has_default_columns?
432 def has_default_columns?
427 column_names.nil? || column_names.empty?
433 column_names.nil? || column_names.empty?
428 end
434 end
429
435
430 def sort_criteria=(arg)
436 def sort_criteria=(arg)
431 c = []
437 c = []
432 if arg.is_a?(Hash)
438 if arg.is_a?(Hash)
433 arg = arg.keys.sort.collect {|k| arg[k]}
439 arg = arg.keys.sort.collect {|k| arg[k]}
434 end
440 end
435 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
441 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
436 write_attribute(:sort_criteria, c)
442 write_attribute(:sort_criteria, c)
437 end
443 end
438
444
439 def sort_criteria
445 def sort_criteria
440 read_attribute(:sort_criteria) || []
446 read_attribute(:sort_criteria) || []
441 end
447 end
442
448
443 def sort_criteria_key(arg)
449 def sort_criteria_key(arg)
444 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
450 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
445 end
451 end
446
452
447 def sort_criteria_order(arg)
453 def sort_criteria_order(arg)
448 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
454 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
449 end
455 end
450
456
451 def sort_criteria_order_for(key)
457 def sort_criteria_order_for(key)
452 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
458 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
453 end
459 end
454
460
455 # Returns the SQL sort order that should be prepended for grouping
461 # Returns the SQL sort order that should be prepended for grouping
456 def group_by_sort_order
462 def group_by_sort_order
457 if grouped? && (column = group_by_column)
463 if grouped? && (column = group_by_column)
458 order = sort_criteria_order_for(column.name) || column.default_order
464 order = sort_criteria_order_for(column.name) || column.default_order
459 column.sortable.is_a?(Array) ?
465 column.sortable.is_a?(Array) ?
460 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
466 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
461 "#{column.sortable} #{order}"
467 "#{column.sortable} #{order}"
462 end
468 end
463 end
469 end
464
470
465 # Returns true if the query is a grouped query
471 # Returns true if the query is a grouped query
466 def grouped?
472 def grouped?
467 !group_by_column.nil?
473 !group_by_column.nil?
468 end
474 end
469
475
470 def group_by_column
476 def group_by_column
471 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
477 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
472 end
478 end
473
479
474 def group_by_statement
480 def group_by_statement
475 group_by_column.try(:groupable)
481 group_by_column.try(:groupable)
476 end
482 end
477
483
478 def project_statement
484 def project_statement
479 project_clauses = []
485 project_clauses = []
480 if project && !project.descendants.active.empty?
486 if project && !project.descendants.active.empty?
481 ids = [project.id]
487 ids = [project.id]
482 if has_filter?("subproject_id")
488 if has_filter?("subproject_id")
483 case operator_for("subproject_id")
489 case operator_for("subproject_id")
484 when '='
490 when '='
485 # include the selected subprojects
491 # include the selected subprojects
486 ids += values_for("subproject_id").each(&:to_i)
492 ids += values_for("subproject_id").each(&:to_i)
487 when '!*'
493 when '!*'
488 # main project only
494 # main project only
489 else
495 else
490 # all subprojects
496 # all subprojects
491 ids += project.descendants.collect(&:id)
497 ids += project.descendants.collect(&:id)
492 end
498 end
493 elsif Setting.display_subprojects_issues?
499 elsif Setting.display_subprojects_issues?
494 ids += project.descendants.collect(&:id)
500 ids += project.descendants.collect(&:id)
495 end
501 end
496 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
502 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
497 elsif project
503 elsif project
498 project_clauses << "#{Project.table_name}.id = %d" % project.id
504 project_clauses << "#{Project.table_name}.id = %d" % project.id
499 end
505 end
500 project_clauses.any? ? project_clauses.join(' AND ') : nil
506 project_clauses.any? ? project_clauses.join(' AND ') : nil
501 end
507 end
502
508
503 def statement
509 def statement
504 # filters clauses
510 # filters clauses
505 filters_clauses = []
511 filters_clauses = []
506 filters.each_key do |field|
512 filters.each_key do |field|
507 next if field == "subproject_id"
513 next if field == "subproject_id"
508 v = values_for(field).clone
514 v = values_for(field).clone
509 next unless v and !v.empty?
515 next unless v and !v.empty?
510 operator = operator_for(field)
516 operator = operator_for(field)
511
517
512 # "me" value subsitution
518 # "me" value subsitution
513 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
519 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
514 if v.delete("me")
520 if v.delete("me")
515 if User.current.logged?
521 if User.current.logged?
516 v.push(User.current.id.to_s)
522 v.push(User.current.id.to_s)
517 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
523 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
518 else
524 else
519 v.push("0")
525 v.push("0")
520 end
526 end
521 end
527 end
522 end
528 end
523
529
524 if field == 'project_id'
530 if field == 'project_id'
525 if v.delete('mine')
531 if v.delete('mine')
526 v += User.current.memberships.map(&:project_id).map(&:to_s)
532 v += User.current.memberships.map(&:project_id).map(&:to_s)
527 end
533 end
528 end
534 end
529
535
530 if field =~ /cf_(\d+)$/
536 if field =~ /cf_(\d+)$/
531 # custom field
537 # custom field
532 filters_clauses << sql_for_custom_field(field, operator, v, $1)
538 filters_clauses << sql_for_custom_field(field, operator, v, $1)
533 elsif respond_to?("sql_for_#{field}_field")
539 elsif respond_to?("sql_for_#{field}_field")
534 # specific statement
540 # specific statement
535 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
541 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
536 else
542 else
537 # regular field
543 # regular field
538 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
544 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
539 end
545 end
540 end if filters and valid?
546 end if filters and valid?
541
547
542 filters_clauses << project_statement
548 filters_clauses << project_statement
543 filters_clauses.reject!(&:blank?)
549 filters_clauses.reject!(&:blank?)
544
550
545 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
551 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
546 end
552 end
547
553
548 private
554 private
549
555
550 def sql_for_custom_field(field, operator, value, custom_field_id)
556 def sql_for_custom_field(field, operator, value, custom_field_id)
551 db_table = CustomValue.table_name
557 db_table = CustomValue.table_name
552 db_field = 'value'
558 db_field = 'value'
553 filter = @available_filters[field]
559 filter = @available_filters[field]
554 return nil unless filter
560 return nil unless filter
555 if filter[:format] == 'user'
561 if filter[:format] == 'user'
556 if value.delete('me')
562 if value.delete('me')
557 value.push User.current.id.to_s
563 value.push User.current.id.to_s
558 end
564 end
559 end
565 end
560 not_in = nil
566 not_in = nil
561 if operator == '!'
567 if operator == '!'
562 # Makes ! operator work for custom fields with multiple values
568 # Makes ! operator work for custom fields with multiple values
563 operator = '='
569 operator = '='
564 not_in = 'NOT'
570 not_in = 'NOT'
565 end
571 end
566 customized_key = "id"
572 customized_key = "id"
567 customized_class = queried_class
573 customized_class = queried_class
568 if field =~ /^(.+)\.cf_/
574 if field =~ /^(.+)\.cf_/
569 assoc = $1
575 assoc = $1
570 customized_key = "#{assoc}_id"
576 customized_key = "#{assoc}_id"
571 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
577 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
572 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
578 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
573 end
579 end
574 "#{queried_table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} 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} WHERE " +
580 "#{queried_table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} 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} WHERE " +
575 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
581 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
576 end
582 end
577
583
578 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
584 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
579 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
585 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
580 sql = ''
586 sql = ''
581 case operator
587 case operator
582 when "="
588 when "="
583 if value.any?
589 if value.any?
584 case type_for(field)
590 case type_for(field)
585 when :date, :date_past
591 when :date, :date_past
586 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
592 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
587 when :integer
593 when :integer
588 if is_custom_filter
594 if is_custom_filter
589 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})"
595 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})"
590 else
596 else
591 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
597 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
592 end
598 end
593 when :float
599 when :float
594 if is_custom_filter
600 if is_custom_filter
595 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})"
601 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})"
596 else
602 else
597 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
603 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
598 end
604 end
599 else
605 else
600 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
606 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
601 end
607 end
602 else
608 else
603 # IN an empty set
609 # IN an empty set
604 sql = "1=0"
610 sql = "1=0"
605 end
611 end
606 when "!"
612 when "!"
607 if value.any?
613 if value.any?
608 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
614 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
609 else
615 else
610 # NOT IN an empty set
616 # NOT IN an empty set
611 sql = "1=1"
617 sql = "1=1"
612 end
618 end
613 when "!*"
619 when "!*"
614 sql = "#{db_table}.#{db_field} IS NULL"
620 sql = "#{db_table}.#{db_field} IS NULL"
615 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
621 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
616 when "*"
622 when "*"
617 sql = "#{db_table}.#{db_field} IS NOT NULL"
623 sql = "#{db_table}.#{db_field} IS NOT NULL"
618 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
624 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
619 when ">="
625 when ">="
620 if [:date, :date_past].include?(type_for(field))
626 if [:date, :date_past].include?(type_for(field))
621 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
627 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
622 else
628 else
623 if is_custom_filter
629 if is_custom_filter
624 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})"
630 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})"
625 else
631 else
626 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
632 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
627 end
633 end
628 end
634 end
629 when "<="
635 when "<="
630 if [:date, :date_past].include?(type_for(field))
636 if [:date, :date_past].include?(type_for(field))
631 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
637 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
632 else
638 else
633 if is_custom_filter
639 if is_custom_filter
634 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})"
640 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})"
635 else
641 else
636 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
642 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
637 end
643 end
638 end
644 end
639 when "><"
645 when "><"
640 if [:date, :date_past].include?(type_for(field))
646 if [:date, :date_past].include?(type_for(field))
641 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
647 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
642 else
648 else
643 if is_custom_filter
649 if is_custom_filter
644 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})"
650 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})"
645 else
651 else
646 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
652 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
647 end
653 end
648 end
654 end
649 when "o"
655 when "o"
650 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
656 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
651 when "c"
657 when "c"
652 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
658 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
653 when "><t-"
659 when "><t-"
654 # between today - n days and today
660 # between today - n days and today
655 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
661 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
656 when ">t-"
662 when ">t-"
657 # >= today - n days
663 # >= today - n days
658 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
664 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
659 when "<t-"
665 when "<t-"
660 # <= today - n days
666 # <= today - n days
661 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
667 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
662 when "t-"
668 when "t-"
663 # = n days in past
669 # = n days in past
664 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
670 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
665 when "><t+"
671 when "><t+"
666 # between today and today + n days
672 # between today and today + n days
667 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
673 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
668 when ">t+"
674 when ">t+"
669 # >= today + n days
675 # >= today + n days
670 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
676 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
671 when "<t+"
677 when "<t+"
672 # <= today + n days
678 # <= today + n days
673 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
679 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
674 when "t+"
680 when "t+"
675 # = today + n days
681 # = today + n days
676 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
682 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
677 when "t"
683 when "t"
678 # = today
684 # = today
679 sql = relative_date_clause(db_table, db_field, 0, 0)
685 sql = relative_date_clause(db_table, db_field, 0, 0)
680 when "ld"
686 when "ld"
681 # = yesterday
687 # = yesterday
682 sql = relative_date_clause(db_table, db_field, -1, -1)
688 sql = relative_date_clause(db_table, db_field, -1, -1)
683 when "w"
689 when "w"
684 # = this week
690 # = this week
685 first_day_of_week = l(:general_first_day_of_week).to_i
691 first_day_of_week = l(:general_first_day_of_week).to_i
686 day_of_week = Date.today.cwday
692 day_of_week = Date.today.cwday
687 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
693 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
688 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
694 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
689 when "lw"
695 when "lw"
690 # = last week
696 # = last week
691 first_day_of_week = l(:general_first_day_of_week).to_i
697 first_day_of_week = l(:general_first_day_of_week).to_i
692 day_of_week = Date.today.cwday
698 day_of_week = Date.today.cwday
693 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
699 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
694 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1)
700 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1)
695 when "l2w"
701 when "l2w"
696 # = last 2 weeks
702 # = last 2 weeks
697 first_day_of_week = l(:general_first_day_of_week).to_i
703 first_day_of_week = l(:general_first_day_of_week).to_i
698 day_of_week = Date.today.cwday
704 day_of_week = Date.today.cwday
699 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
705 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
700 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1)
706 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1)
701 when "m"
707 when "m"
702 # = this month
708 # = this month
703 date = Date.today
709 date = Date.today
704 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
710 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
705 when "lm"
711 when "lm"
706 # = last month
712 # = last month
707 date = Date.today.prev_month
713 date = Date.today.prev_month
708 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
714 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
709 when "y"
715 when "y"
710 # = this year
716 # = this year
711 date = Date.today
717 date = Date.today
712 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year)
718 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year)
713 when "~"
719 when "~"
714 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
720 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
715 when "!~"
721 when "!~"
716 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
722 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
717 else
723 else
718 raise "Unknown query operator #{operator}"
724 raise "Unknown query operator #{operator}"
719 end
725 end
720
726
721 return sql
727 return sql
722 end
728 end
723
729
724 def add_custom_fields_filters(custom_fields, assoc=nil)
730 def add_custom_fields_filters(custom_fields, assoc=nil)
725 return unless custom_fields.present?
731 return unless custom_fields.present?
726
732
727 custom_fields.select(&:is_filter?).sort.each do |field|
733 custom_fields.select(&:is_filter?).sort.each do |field|
728 case field.field_format
734 case field.field_format
729 when "text"
735 when "text"
730 options = { :type => :text }
736 options = { :type => :text }
731 when "list"
737 when "list"
732 options = { :type => :list_optional, :values => field.possible_values }
738 options = { :type => :list_optional, :values => field.possible_values }
733 when "date"
739 when "date"
734 options = { :type => :date }
740 options = { :type => :date }
735 when "bool"
741 when "bool"
736 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
742 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
737 when "int"
743 when "int"
738 options = { :type => :integer }
744 options = { :type => :integer }
739 when "float"
745 when "float"
740 options = { :type => :float }
746 options = { :type => :float }
741 when "user", "version"
747 when "user", "version"
742 next unless project
748 next unless project
743 values = field.possible_values_options(project)
749 values = field.possible_values_options(project)
744 if User.current.logged? && field.field_format == 'user'
750 if User.current.logged? && field.field_format == 'user'
745 values.unshift ["<< #{l(:label_me)} >>", "me"]
751 values.unshift ["<< #{l(:label_me)} >>", "me"]
746 end
752 end
747 options = { :type => :list_optional, :values => values }
753 options = { :type => :list_optional, :values => values }
748 else
754 else
749 options = { :type => :string }
755 options = { :type => :string }
750 end
756 end
751 filter_id = "cf_#{field.id}"
757 filter_id = "cf_#{field.id}"
752 filter_name = field.name
758 filter_name = field.name
753 if assoc.present?
759 if assoc.present?
754 filter_id = "#{assoc}.#{filter_id}"
760 filter_id = "#{assoc}.#{filter_id}"
755 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
761 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
756 end
762 end
757 add_available_filter filter_id, options.merge({
763 add_available_filter filter_id, options.merge({
758 :name => filter_name,
764 :name => filter_name,
759 :format => field.field_format,
765 :format => field.field_format,
760 :field => field
766 :field => field
761 })
767 })
762 end
768 end
763 end
769 end
764
770
765 def add_associations_custom_fields_filters(*associations)
771 def add_associations_custom_fields_filters(*associations)
766 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
772 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
767 associations.each do |assoc|
773 associations.each do |assoc|
768 association_klass = queried_class.reflect_on_association(assoc).klass
774 association_klass = queried_class.reflect_on_association(assoc).klass
769 fields_by_class.each do |field_class, fields|
775 fields_by_class.each do |field_class, fields|
770 if field_class.customized_class <= association_klass
776 if field_class.customized_class <= association_klass
771 add_custom_fields_filters(fields, assoc)
777 add_custom_fields_filters(fields, assoc)
772 end
778 end
773 end
779 end
774 end
780 end
775 end
781 end
776
782
777 # Returns a SQL clause for a date or datetime field.
783 # Returns a SQL clause for a date or datetime field.
778 def date_clause(table, field, from, to)
784 def date_clause(table, field, from, to)
779 s = []
785 s = []
780 if from
786 if from
781 from_yesterday = from - 1
787 from_yesterday = from - 1
782 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
788 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
783 if self.class.default_timezone == :utc
789 if self.class.default_timezone == :utc
784 from_yesterday_time = from_yesterday_time.utc
790 from_yesterday_time = from_yesterday_time.utc
785 end
791 end
786 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
792 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
787 end
793 end
788 if to
794 if to
789 to_time = Time.local(to.year, to.month, to.day)
795 to_time = Time.local(to.year, to.month, to.day)
790 if self.class.default_timezone == :utc
796 if self.class.default_timezone == :utc
791 to_time = to_time.utc
797 to_time = to_time.utc
792 end
798 end
793 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
799 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
794 end
800 end
795 s.join(' AND ')
801 s.join(' AND ')
796 end
802 end
797
803
798 # Returns a SQL clause for a date or datetime field using relative dates.
804 # Returns a SQL clause for a date or datetime field using relative dates.
799 def relative_date_clause(table, field, days_from, days_to)
805 def relative_date_clause(table, field, days_from, days_to)
800 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
806 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
801 end
807 end
802
808
803 # Additional joins required for the given sort options
809 # Additional joins required for the given sort options
804 def joins_for_order_statement(order_options)
810 def joins_for_order_statement(order_options)
805 joins = []
811 joins = []
806
812
807 if order_options
813 if order_options
808 if order_options.include?('authors')
814 if order_options.include?('authors')
809 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
815 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
810 end
816 end
811 order_options.scan(/cf_\d+/).uniq.each do |name|
817 order_options.scan(/cf_\d+/).uniq.each do |name|
812 column = available_columns.detect {|c| c.name.to_s == name}
818 column = available_columns.detect {|c| c.name.to_s == name}
813 join = column && column.custom_field.join_for_order_statement
819 join = column && column.custom_field.join_for_order_statement
814 if join
820 if join
815 joins << join
821 joins << join
816 end
822 end
817 end
823 end
818 end
824 end
819
825
820 joins.any? ? joins.join(' ') : nil
826 joins.any? ? joins.join(' ') : nil
821 end
827 end
822 end
828 end
@@ -1,49 +1,47
1 <%= form_tag({}) do -%>
1 <%= form_tag({}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
2 <%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
3 <div class="autoscroll">
3 <div class="autoscroll">
4 <table class="list issues">
4 <table class="list issues">
5 <thead>
5 <thead>
6 <tr>
6 <tr>
7 <th class="checkbox hide-when-print">
7 <th class="checkbox hide-when-print">
8 <%= link_to image_tag('toggle_check.png'), {},
8 <%= link_to image_tag('toggle_check.png'), {},
9 :onclick => 'toggleIssuesSelection(this); return false;',
9 :onclick => 'toggleIssuesSelection(this); return false;',
10 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
10 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
11 </th>
11 </th>
12 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
13 <% query.inline_columns.each do |column| %>
12 <% query.inline_columns.each do |column| %>
14 <%= column_header(column) %>
13 <%= column_header(column) %>
15 <% end %>
14 <% end %>
16 </tr>
15 </tr>
17 </thead>
16 </thead>
18 <% previous_group = false %>
17 <% previous_group = false %>
19 <tbody>
18 <tbody>
20 <% issue_list(issues) do |issue, level| -%>
19 <% issue_list(issues) do |issue, level| -%>
21 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
20 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
22 <% reset_cycle %>
21 <% reset_cycle %>
23 <tr class="group open">
22 <tr class="group open">
24 <td colspan="<%= query.inline_columns.size + 2 %>">
23 <td colspan="<%= query.inline_columns.size + 2 %>">
25 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
24 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
26 <%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, issue) %> <span class="count"><%= @issue_count_by_group[group] %></span>
25 <%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, issue) %> <span class="count"><%= @issue_count_by_group[group] %></span>
27 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
26 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
28 "toggleAllRowGroups(this)", :class => 'toggle-all') %>
27 "toggleAllRowGroups(this)", :class => 'toggle-all') %>
29 </td>
28 </td>
30 </tr>
29 </tr>
31 <% previous_group = group %>
30 <% previous_group = group %>
32 <% end %>
31 <% end %>
33 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
32 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
34 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
33 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
35 <td class="id"><%= link_to issue.id, issue_path(issue) %></td>
36 <%= raw query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
34 <%= raw query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
37 </tr>
35 </tr>
38 <% @query.block_columns.each do |column|
36 <% @query.block_columns.each do |column|
39 if (text = column_content(column, issue)) && text.present? -%>
37 if (text = column_content(column, issue)) && text.present? -%>
40 <tr class="<%= current_cycle %>">
38 <tr class="<%= current_cycle %>">
41 <td colspan="<%= @query.inline_columns.size + 2 %>" class="<%= column.css_classes %>"><%= text %></td>
39 <td colspan="<%= @query.inline_columns.size + 1 %>" class="<%= column.css_classes %>"><%= text %></td>
42 </tr>
40 </tr>
43 <% end -%>
41 <% end -%>
44 <% end -%>
42 <% end -%>
45 <% end -%>
43 <% end -%>
46 </tbody>
44 </tbody>
47 </table>
45 </table>
48 </div>
46 </div>
49 <% end -%>
47 <% end -%>
@@ -1,34 +1,34
1 <table class="query-columns">
1 <table class="query-columns">
2 <tr>
2 <tr>
3 <td style="padding-left:0">
3 <td style="padding-left:0">
4 <%= label_tag "available_columns", l(:description_available_columns) %>
4 <%= label_tag "available_columns", l(:description_available_columns) %>
5 <br />
5 <br />
6 <%= select_tag 'available_columns',
6 <%= select_tag 'available_columns',
7 options_for_select((query.available_inline_columns - query.columns).collect {|column| [column.caption, column.name]}),
7 options_for_select((query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}),
8 :multiple => true, :size => 10, :style => "width:150px",
8 :multiple => true, :size => 10, :style => "width:150px",
9 :ondblclick => "moveOptions(this.form.available_columns, this.form.selected_columns);" %>
9 :ondblclick => "moveOptions(this.form.available_columns, this.form.selected_columns);" %>
10 </td>
10 </td>
11 <td class="buttons">
11 <td class="buttons">
12 <input type="button" value="&#8594;"
12 <input type="button" value="&#8594;"
13 onclick="moveOptions(this.form.available_columns, this.form.selected_columns);" /><br />
13 onclick="moveOptions(this.form.available_columns, this.form.selected_columns);" /><br />
14 <input type="button" value="&#8592;"
14 <input type="button" value="&#8592;"
15 onclick="moveOptions(this.form.selected_columns, this.form.available_columns);" />
15 onclick="moveOptions(this.form.selected_columns, this.form.available_columns);" />
16 </td>
16 </td>
17 <td>
17 <td>
18 <%= label_tag "selected_columns", l(:description_selected_columns) %>
18 <%= label_tag "selected_columns", l(:description_selected_columns) %>
19 <br />
19 <br />
20 <%= select_tag((defined?(tag_name) ? tag_name : 'c[]'),
20 <%= select_tag((defined?(tag_name) ? tag_name : 'c[]'),
21 options_for_select(query.inline_columns.collect {|column| [column.caption, column.name]}),
21 options_for_select((query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}),
22 :id => 'selected_columns', :multiple => true, :size => 10, :style => "width:150px",
22 :id => 'selected_columns', :multiple => true, :size => 10, :style => "width:150px",
23 :ondblclick => "moveOptions(this.form.selected_columns, this.form.available_columns);") %>
23 :ondblclick => "moveOptions(this.form.selected_columns, this.form.available_columns);") %>
24 </td>
24 </td>
25 <td class="buttons">
25 <td class="buttons">
26 <input type="button" value="&#8593;" onclick="moveOptionUp(this.form.selected_columns);" /><br />
26 <input type="button" value="&#8593;" onclick="moveOptionUp(this.form.selected_columns);" /><br />
27 <input type="button" value="&#8595;" onclick="moveOptionDown(this.form.selected_columns);" />
27 <input type="button" value="&#8595;" onclick="moveOptionDown(this.form.selected_columns);" />
28 </td>
28 </td>
29 </tr>
29 </tr>
30 </table>
30 </table>
31
31
32 <% content_for :header_tags do %>
32 <% content_for :header_tags do %>
33 <%= javascript_include_tag 'select_list_move' %>
33 <%= javascript_include_tag 'select_list_move' %>
34 <% end %>
34 <% end %>
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,290 +1,290
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueriesControllerTest < ActionController::TestCase
20 class QueriesControllerTest < ActionController::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules
21 fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules
22
22
23 def setup
23 def setup
24 User.current = nil
24 User.current = nil
25 end
25 end
26
26
27 def test_index
27 def test_index
28 get :index
28 get :index
29 # HTML response not implemented
29 # HTML response not implemented
30 assert_response 406
30 assert_response 406
31 end
31 end
32
32
33 def test_new_project_query
33 def test_new_project_query
34 @request.session[:user_id] = 2
34 @request.session[:user_id] = 2
35 get :new, :project_id => 1
35 get :new, :project_id => 1
36 assert_response :success
36 assert_response :success
37 assert_template 'new'
37 assert_template 'new'
38 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
38 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
39 :name => 'query[is_public]',
39 :name => 'query[is_public]',
40 :checked => nil }
40 :checked => nil }
41 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
41 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
42 :name => 'query_is_for_all',
42 :name => 'query_is_for_all',
43 :checked => nil,
43 :checked => nil,
44 :disabled => nil }
44 :disabled => nil }
45 assert_select 'select[name=?]', 'c[]' do
45 assert_select 'select[name=?]', 'c[]' do
46 assert_select 'option[value=tracker]'
46 assert_select 'option[value=tracker]'
47 assert_select 'option[value=subject]'
47 assert_select 'option[value=subject]'
48 end
48 end
49 end
49 end
50
50
51 def test_new_global_query
51 def test_new_global_query
52 @request.session[:user_id] = 2
52 @request.session[:user_id] = 2
53 get :new
53 get :new
54 assert_response :success
54 assert_response :success
55 assert_template 'new'
55 assert_template 'new'
56 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
56 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
57 :name => 'query[is_public]' }
57 :name => 'query[is_public]' }
58 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
58 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
59 :name => 'query_is_for_all',
59 :name => 'query_is_for_all',
60 :checked => 'checked',
60 :checked => 'checked',
61 :disabled => nil }
61 :disabled => nil }
62 end
62 end
63
63
64 def test_new_on_invalid_project
64 def test_new_on_invalid_project
65 @request.session[:user_id] = 2
65 @request.session[:user_id] = 2
66 get :new, :project_id => 'invalid'
66 get :new, :project_id => 'invalid'
67 assert_response 404
67 assert_response 404
68 end
68 end
69
69
70 def test_create_project_public_query
70 def test_create_project_public_query
71 @request.session[:user_id] = 2
71 @request.session[:user_id] = 2
72 post :create,
72 post :create,
73 :project_id => 'ecookbook',
73 :project_id => 'ecookbook',
74 :default_columns => '1',
74 :default_columns => '1',
75 :f => ["status_id", "assigned_to_id"],
75 :f => ["status_id", "assigned_to_id"],
76 :op => {"assigned_to_id" => "=", "status_id" => "o"},
76 :op => {"assigned_to_id" => "=", "status_id" => "o"},
77 :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
77 :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
78 :query => {"name" => "test_new_project_public_query", "is_public" => "1"}
78 :query => {"name" => "test_new_project_public_query", "is_public" => "1"}
79
79
80 q = Query.find_by_name('test_new_project_public_query')
80 q = Query.find_by_name('test_new_project_public_query')
81 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
81 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
82 assert q.is_public?
82 assert q.is_public?
83 assert q.has_default_columns?
83 assert q.has_default_columns?
84 assert q.valid?
84 assert q.valid?
85 end
85 end
86
86
87 def test_create_project_private_query
87 def test_create_project_private_query
88 @request.session[:user_id] = 3
88 @request.session[:user_id] = 3
89 post :create,
89 post :create,
90 :project_id => 'ecookbook',
90 :project_id => 'ecookbook',
91 :default_columns => '1',
91 :default_columns => '1',
92 :fields => ["status_id", "assigned_to_id"],
92 :fields => ["status_id", "assigned_to_id"],
93 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
93 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
94 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
94 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
95 :query => {"name" => "test_new_project_private_query", "is_public" => "1"}
95 :query => {"name" => "test_new_project_private_query", "is_public" => "1"}
96
96
97 q = Query.find_by_name('test_new_project_private_query')
97 q = Query.find_by_name('test_new_project_private_query')
98 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
98 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
99 assert !q.is_public?
99 assert !q.is_public?
100 assert q.has_default_columns?
100 assert q.has_default_columns?
101 assert q.valid?
101 assert q.valid?
102 end
102 end
103
103
104 def test_create_global_private_query_with_custom_columns
104 def test_create_global_private_query_with_custom_columns
105 @request.session[:user_id] = 3
105 @request.session[:user_id] = 3
106 post :create,
106 post :create,
107 :fields => ["status_id", "assigned_to_id"],
107 :fields => ["status_id", "assigned_to_id"],
108 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
108 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
109 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
109 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
110 :query => {"name" => "test_new_global_private_query", "is_public" => "1"},
110 :query => {"name" => "test_new_global_private_query", "is_public" => "1"},
111 :c => ["", "tracker", "subject", "priority", "category"]
111 :c => ["", "tracker", "subject", "priority", "category"]
112
112
113 q = Query.find_by_name('test_new_global_private_query')
113 q = Query.find_by_name('test_new_global_private_query')
114 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
114 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
115 assert !q.is_public?
115 assert !q.is_public?
116 assert !q.has_default_columns?
116 assert !q.has_default_columns?
117 assert_equal [:tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
117 assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
118 assert q.valid?
118 assert q.valid?
119 end
119 end
120
120
121 def test_create_global_query_with_custom_filters
121 def test_create_global_query_with_custom_filters
122 @request.session[:user_id] = 3
122 @request.session[:user_id] = 3
123 post :create,
123 post :create,
124 :fields => ["assigned_to_id"],
124 :fields => ["assigned_to_id"],
125 :operators => {"assigned_to_id" => "="},
125 :operators => {"assigned_to_id" => "="},
126 :values => { "assigned_to_id" => ["me"]},
126 :values => { "assigned_to_id" => ["me"]},
127 :query => {"name" => "test_new_global_query"}
127 :query => {"name" => "test_new_global_query"}
128
128
129 q = Query.find_by_name('test_new_global_query')
129 q = Query.find_by_name('test_new_global_query')
130 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
130 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
131 assert !q.has_filter?(:status_id)
131 assert !q.has_filter?(:status_id)
132 assert_equal ['assigned_to_id'], q.filters.keys
132 assert_equal ['assigned_to_id'], q.filters.keys
133 assert q.valid?
133 assert q.valid?
134 end
134 end
135
135
136 def test_create_with_sort
136 def test_create_with_sort
137 @request.session[:user_id] = 1
137 @request.session[:user_id] = 1
138 post :create,
138 post :create,
139 :default_columns => '1',
139 :default_columns => '1',
140 :operators => {"status_id" => "o"},
140 :operators => {"status_id" => "o"},
141 :values => {"status_id" => ["1"]},
141 :values => {"status_id" => ["1"]},
142 :query => {:name => "test_new_with_sort",
142 :query => {:name => "test_new_with_sort",
143 :is_public => "1",
143 :is_public => "1",
144 :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
144 :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
145
145
146 query = Query.find_by_name("test_new_with_sort")
146 query = Query.find_by_name("test_new_with_sort")
147 assert_not_nil query
147 assert_not_nil query
148 assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
148 assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
149 end
149 end
150
150
151 def test_create_with_failure
151 def test_create_with_failure
152 @request.session[:user_id] = 2
152 @request.session[:user_id] = 2
153 assert_no_difference '::Query.count' do
153 assert_no_difference '::Query.count' do
154 post :create, :project_id => 'ecookbook', :query => {:name => ''}
154 post :create, :project_id => 'ecookbook', :query => {:name => ''}
155 end
155 end
156 assert_response :success
156 assert_response :success
157 assert_template 'new'
157 assert_template 'new'
158 assert_select 'input[name=?]', 'query[name]'
158 assert_select 'input[name=?]', 'query[name]'
159 end
159 end
160
160
161 def test_edit_global_public_query
161 def test_edit_global_public_query
162 @request.session[:user_id] = 1
162 @request.session[:user_id] = 1
163 get :edit, :id => 4
163 get :edit, :id => 4
164 assert_response :success
164 assert_response :success
165 assert_template 'edit'
165 assert_template 'edit'
166 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
166 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
167 :name => 'query[is_public]',
167 :name => 'query[is_public]',
168 :checked => 'checked' }
168 :checked => 'checked' }
169 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
169 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
170 :name => 'query_is_for_all',
170 :name => 'query_is_for_all',
171 :checked => 'checked',
171 :checked => 'checked',
172 :disabled => 'disabled' }
172 :disabled => 'disabled' }
173 end
173 end
174
174
175 def test_edit_global_private_query
175 def test_edit_global_private_query
176 @request.session[:user_id] = 3
176 @request.session[:user_id] = 3
177 get :edit, :id => 3
177 get :edit, :id => 3
178 assert_response :success
178 assert_response :success
179 assert_template 'edit'
179 assert_template 'edit'
180 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
180 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
181 :name => 'query[is_public]' }
181 :name => 'query[is_public]' }
182 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
182 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
183 :name => 'query_is_for_all',
183 :name => 'query_is_for_all',
184 :checked => 'checked',
184 :checked => 'checked',
185 :disabled => 'disabled' }
185 :disabled => 'disabled' }
186 end
186 end
187
187
188 def test_edit_project_private_query
188 def test_edit_project_private_query
189 @request.session[:user_id] = 3
189 @request.session[:user_id] = 3
190 get :edit, :id => 2
190 get :edit, :id => 2
191 assert_response :success
191 assert_response :success
192 assert_template 'edit'
192 assert_template 'edit'
193 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
193 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
194 :name => 'query[is_public]' }
194 :name => 'query[is_public]' }
195 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
195 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
196 :name => 'query_is_for_all',
196 :name => 'query_is_for_all',
197 :checked => nil,
197 :checked => nil,
198 :disabled => nil }
198 :disabled => nil }
199 end
199 end
200
200
201 def test_edit_project_public_query
201 def test_edit_project_public_query
202 @request.session[:user_id] = 2
202 @request.session[:user_id] = 2
203 get :edit, :id => 1
203 get :edit, :id => 1
204 assert_response :success
204 assert_response :success
205 assert_template 'edit'
205 assert_template 'edit'
206 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
206 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
207 :name => 'query[is_public]',
207 :name => 'query[is_public]',
208 :checked => 'checked'
208 :checked => 'checked'
209 }
209 }
210 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
210 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
211 :name => 'query_is_for_all',
211 :name => 'query_is_for_all',
212 :checked => nil,
212 :checked => nil,
213 :disabled => 'disabled' }
213 :disabled => 'disabled' }
214 end
214 end
215
215
216 def test_edit_sort_criteria
216 def test_edit_sort_criteria
217 @request.session[:user_id] = 1
217 @request.session[:user_id] = 1
218 get :edit, :id => 5
218 get :edit, :id => 5
219 assert_response :success
219 assert_response :success
220 assert_template 'edit'
220 assert_template 'edit'
221 assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
221 assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
222 :child => { :tag => 'option', :attributes => { :value => 'priority',
222 :child => { :tag => 'option', :attributes => { :value => 'priority',
223 :selected => 'selected' } }
223 :selected => 'selected' } }
224 assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
224 assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
225 :child => { :tag => 'option', :attributes => { :value => 'desc',
225 :child => { :tag => 'option', :attributes => { :value => 'desc',
226 :selected => 'selected' } }
226 :selected => 'selected' } }
227 end
227 end
228
228
229 def test_edit_invalid_query
229 def test_edit_invalid_query
230 @request.session[:user_id] = 2
230 @request.session[:user_id] = 2
231 get :edit, :id => 99
231 get :edit, :id => 99
232 assert_response 404
232 assert_response 404
233 end
233 end
234
234
235 def test_udpate_global_private_query
235 def test_udpate_global_private_query
236 @request.session[:user_id] = 3
236 @request.session[:user_id] = 3
237 put :update,
237 put :update,
238 :id => 3,
238 :id => 3,
239 :default_columns => '1',
239 :default_columns => '1',
240 :fields => ["status_id", "assigned_to_id"],
240 :fields => ["status_id", "assigned_to_id"],
241 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
241 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
242 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
242 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
243 :query => {"name" => "test_edit_global_private_query", "is_public" => "1"}
243 :query => {"name" => "test_edit_global_private_query", "is_public" => "1"}
244
244
245 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
245 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
246 q = Query.find_by_name('test_edit_global_private_query')
246 q = Query.find_by_name('test_edit_global_private_query')
247 assert !q.is_public?
247 assert !q.is_public?
248 assert q.has_default_columns?
248 assert q.has_default_columns?
249 assert q.valid?
249 assert q.valid?
250 end
250 end
251
251
252 def test_update_global_public_query
252 def test_update_global_public_query
253 @request.session[:user_id] = 1
253 @request.session[:user_id] = 1
254 put :update,
254 put :update,
255 :id => 4,
255 :id => 4,
256 :default_columns => '1',
256 :default_columns => '1',
257 :fields => ["status_id", "assigned_to_id"],
257 :fields => ["status_id", "assigned_to_id"],
258 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
258 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
259 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
259 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
260 :query => {"name" => "test_edit_global_public_query", "is_public" => "1"}
260 :query => {"name" => "test_edit_global_public_query", "is_public" => "1"}
261
261
262 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
262 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
263 q = Query.find_by_name('test_edit_global_public_query')
263 q = Query.find_by_name('test_edit_global_public_query')
264 assert q.is_public?
264 assert q.is_public?
265 assert q.has_default_columns?
265 assert q.has_default_columns?
266 assert q.valid?
266 assert q.valid?
267 end
267 end
268
268
269 def test_update_with_failure
269 def test_update_with_failure
270 @request.session[:user_id] = 1
270 @request.session[:user_id] = 1
271 put :update, :id => 4, :query => {:name => ''}
271 put :update, :id => 4, :query => {:name => ''}
272 assert_response :success
272 assert_response :success
273 assert_template 'edit'
273 assert_template 'edit'
274 end
274 end
275
275
276 def test_destroy
276 def test_destroy
277 @request.session[:user_id] = 2
277 @request.session[:user_id] = 2
278 delete :destroy, :id => 1
278 delete :destroy, :id => 1
279 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
279 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
280 assert_nil Query.find_by_id(1)
280 assert_nil Query.find_by_id(1)
281 end
281 end
282
282
283 def test_backslash_should_be_escaped_in_filters
283 def test_backslash_should_be_escaped_in_filters
284 @request.session[:user_id] = 2
284 @request.session[:user_id] = 2
285 get :new, :subject => 'foo/bar'
285 get :new, :subject => 'foo/bar'
286 assert_response :success
286 assert_response :success
287 assert_template 'new'
287 assert_template 'new'
288 assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body
288 assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body
289 end
289 end
290 end
290 end
@@ -1,1216 +1,1234
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 fixtures :projects, :enabled_modules, :users, :members,
23 fixtures :projects, :enabled_modules, :users, :members,
24 :member_roles, :roles, :trackers, :issue_statuses,
24 :member_roles, :roles, :trackers, :issue_statuses,
25 :issue_categories, :enumerations, :issues,
25 :issue_categories, :enumerations, :issues,
26 :watchers, :custom_fields, :custom_values, :versions,
26 :watchers, :custom_fields, :custom_values, :versions,
27 :queries,
27 :queries,
28 :projects_trackers,
28 :projects_trackers,
29 :custom_fields_trackers
29 :custom_fields_trackers
30
30
31 def test_available_filters_should_be_ordered
31 def test_available_filters_should_be_ordered
32 query = IssueQuery.new
32 query = IssueQuery.new
33 assert_equal 0, query.available_filters.keys.index('status_id')
33 assert_equal 0, query.available_filters.keys.index('status_id')
34 end
34 end
35
35
36 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
36 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
37 query = IssueQuery.new(:project => nil, :name => '_')
37 query = IssueQuery.new(:project => nil, :name => '_')
38 assert query.available_filters.has_key?('cf_1')
38 assert query.available_filters.has_key?('cf_1')
39 assert !query.available_filters.has_key?('cf_3')
39 assert !query.available_filters.has_key?('cf_3')
40 end
40 end
41
41
42 def test_system_shared_versions_should_be_available_in_global_queries
42 def test_system_shared_versions_should_be_available_in_global_queries
43 Version.find(2).update_attribute :sharing, 'system'
43 Version.find(2).update_attribute :sharing, 'system'
44 query = IssueQuery.new(:project => nil, :name => '_')
44 query = IssueQuery.new(:project => nil, :name => '_')
45 assert query.available_filters.has_key?('fixed_version_id')
45 assert query.available_filters.has_key?('fixed_version_id')
46 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
46 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
47 end
47 end
48
48
49 def test_project_filter_in_global_queries
49 def test_project_filter_in_global_queries
50 query = IssueQuery.new(:project => nil, :name => '_')
50 query = IssueQuery.new(:project => nil, :name => '_')
51 project_filter = query.available_filters["project_id"]
51 project_filter = query.available_filters["project_id"]
52 assert_not_nil project_filter
52 assert_not_nil project_filter
53 project_ids = project_filter[:values].map{|p| p[1]}
53 project_ids = project_filter[:values].map{|p| p[1]}
54 assert project_ids.include?("1") #public project
54 assert project_ids.include?("1") #public project
55 assert !project_ids.include?("2") #private project user cannot see
55 assert !project_ids.include?("2") #private project user cannot see
56 end
56 end
57
57
58 def find_issues_with_query(query)
58 def find_issues_with_query(query)
59 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
59 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
60 query.statement
60 query.statement
61 ).all
61 ).all
62 end
62 end
63
63
64 def assert_find_issues_with_query_is_successful(query)
64 def assert_find_issues_with_query_is_successful(query)
65 assert_nothing_raised do
65 assert_nothing_raised do
66 find_issues_with_query(query)
66 find_issues_with_query(query)
67 end
67 end
68 end
68 end
69
69
70 def assert_query_statement_includes(query, condition)
70 def assert_query_statement_includes(query, condition)
71 assert_include condition, query.statement
71 assert_include condition, query.statement
72 end
72 end
73
73
74 def assert_query_result(expected, query)
74 def assert_query_result(expected, query)
75 assert_nothing_raised do
75 assert_nothing_raised do
76 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
76 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
77 assert_equal expected.size, query.issue_count
77 assert_equal expected.size, query.issue_count
78 end
78 end
79 end
79 end
80
80
81 def test_query_should_allow_shared_versions_for_a_project_query
81 def test_query_should_allow_shared_versions_for_a_project_query
82 subproject_version = Version.find(4)
82 subproject_version = Version.find(4)
83 query = IssueQuery.new(:project => Project.find(1), :name => '_')
83 query = IssueQuery.new(:project => Project.find(1), :name => '_')
84 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
84 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
85
85
86 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
86 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
87 end
87 end
88
88
89 def test_query_with_multiple_custom_fields
89 def test_query_with_multiple_custom_fields
90 query = IssueQuery.find(1)
90 query = IssueQuery.find(1)
91 assert query.valid?
91 assert query.valid?
92 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
92 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
93 issues = find_issues_with_query(query)
93 issues = find_issues_with_query(query)
94 assert_equal 1, issues.length
94 assert_equal 1, issues.length
95 assert_equal Issue.find(3), issues.first
95 assert_equal Issue.find(3), issues.first
96 end
96 end
97
97
98 def test_operator_none
98 def test_operator_none
99 query = IssueQuery.new(:project => Project.find(1), :name => '_')
99 query = IssueQuery.new(:project => Project.find(1), :name => '_')
100 query.add_filter('fixed_version_id', '!*', [''])
100 query.add_filter('fixed_version_id', '!*', [''])
101 query.add_filter('cf_1', '!*', [''])
101 query.add_filter('cf_1', '!*', [''])
102 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
102 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
103 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
103 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
104 find_issues_with_query(query)
104 find_issues_with_query(query)
105 end
105 end
106
106
107 def test_operator_none_for_integer
107 def test_operator_none_for_integer
108 query = IssueQuery.new(:project => Project.find(1), :name => '_')
108 query = IssueQuery.new(:project => Project.find(1), :name => '_')
109 query.add_filter('estimated_hours', '!*', [''])
109 query.add_filter('estimated_hours', '!*', [''])
110 issues = find_issues_with_query(query)
110 issues = find_issues_with_query(query)
111 assert !issues.empty?
111 assert !issues.empty?
112 assert issues.all? {|i| !i.estimated_hours}
112 assert issues.all? {|i| !i.estimated_hours}
113 end
113 end
114
114
115 def test_operator_none_for_date
115 def test_operator_none_for_date
116 query = IssueQuery.new(:project => Project.find(1), :name => '_')
116 query = IssueQuery.new(:project => Project.find(1), :name => '_')
117 query.add_filter('start_date', '!*', [''])
117 query.add_filter('start_date', '!*', [''])
118 issues = find_issues_with_query(query)
118 issues = find_issues_with_query(query)
119 assert !issues.empty?
119 assert !issues.empty?
120 assert issues.all? {|i| i.start_date.nil?}
120 assert issues.all? {|i| i.start_date.nil?}
121 end
121 end
122
122
123 def test_operator_none_for_string_custom_field
123 def test_operator_none_for_string_custom_field
124 query = IssueQuery.new(:project => Project.find(1), :name => '_')
124 query = IssueQuery.new(:project => Project.find(1), :name => '_')
125 query.add_filter('cf_2', '!*', [''])
125 query.add_filter('cf_2', '!*', [''])
126 assert query.has_filter?('cf_2')
126 assert query.has_filter?('cf_2')
127 issues = find_issues_with_query(query)
127 issues = find_issues_with_query(query)
128 assert !issues.empty?
128 assert !issues.empty?
129 assert issues.all? {|i| i.custom_field_value(2).blank?}
129 assert issues.all? {|i| i.custom_field_value(2).blank?}
130 end
130 end
131
131
132 def test_operator_all
132 def test_operator_all
133 query = IssueQuery.new(:project => Project.find(1), :name => '_')
133 query = IssueQuery.new(:project => Project.find(1), :name => '_')
134 query.add_filter('fixed_version_id', '*', [''])
134 query.add_filter('fixed_version_id', '*', [''])
135 query.add_filter('cf_1', '*', [''])
135 query.add_filter('cf_1', '*', [''])
136 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
136 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
137 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
137 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
138 find_issues_with_query(query)
138 find_issues_with_query(query)
139 end
139 end
140
140
141 def test_operator_all_for_date
141 def test_operator_all_for_date
142 query = IssueQuery.new(:project => Project.find(1), :name => '_')
142 query = IssueQuery.new(:project => Project.find(1), :name => '_')
143 query.add_filter('start_date', '*', [''])
143 query.add_filter('start_date', '*', [''])
144 issues = find_issues_with_query(query)
144 issues = find_issues_with_query(query)
145 assert !issues.empty?
145 assert !issues.empty?
146 assert issues.all? {|i| i.start_date.present?}
146 assert issues.all? {|i| i.start_date.present?}
147 end
147 end
148
148
149 def test_operator_all_for_string_custom_field
149 def test_operator_all_for_string_custom_field
150 query = IssueQuery.new(:project => Project.find(1), :name => '_')
150 query = IssueQuery.new(:project => Project.find(1), :name => '_')
151 query.add_filter('cf_2', '*', [''])
151 query.add_filter('cf_2', '*', [''])
152 assert query.has_filter?('cf_2')
152 assert query.has_filter?('cf_2')
153 issues = find_issues_with_query(query)
153 issues = find_issues_with_query(query)
154 assert !issues.empty?
154 assert !issues.empty?
155 assert issues.all? {|i| i.custom_field_value(2).present?}
155 assert issues.all? {|i| i.custom_field_value(2).present?}
156 end
156 end
157
157
158 def test_numeric_filter_should_not_accept_non_numeric_values
158 def test_numeric_filter_should_not_accept_non_numeric_values
159 query = IssueQuery.new(:name => '_')
159 query = IssueQuery.new(:name => '_')
160 query.add_filter('estimated_hours', '=', ['a'])
160 query.add_filter('estimated_hours', '=', ['a'])
161
161
162 assert query.has_filter?('estimated_hours')
162 assert query.has_filter?('estimated_hours')
163 assert !query.valid?
163 assert !query.valid?
164 end
164 end
165
165
166 def test_operator_is_on_float
166 def test_operator_is_on_float
167 Issue.update_all("estimated_hours = 171.2", "id=2")
167 Issue.update_all("estimated_hours = 171.2", "id=2")
168
168
169 query = IssueQuery.new(:name => '_')
169 query = IssueQuery.new(:name => '_')
170 query.add_filter('estimated_hours', '=', ['171.20'])
170 query.add_filter('estimated_hours', '=', ['171.20'])
171 issues = find_issues_with_query(query)
171 issues = find_issues_with_query(query)
172 assert_equal 1, issues.size
172 assert_equal 1, issues.size
173 assert_equal 2, issues.first.id
173 assert_equal 2, issues.first.id
174 end
174 end
175
175
176 def test_operator_is_on_integer_custom_field
176 def test_operator_is_on_integer_custom_field
177 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
177 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
178 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
178 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
179 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
179 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
180 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
180 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
181
181
182 query = IssueQuery.new(:name => '_')
182 query = IssueQuery.new(:name => '_')
183 query.add_filter("cf_#{f.id}", '=', ['12'])
183 query.add_filter("cf_#{f.id}", '=', ['12'])
184 issues = find_issues_with_query(query)
184 issues = find_issues_with_query(query)
185 assert_equal 1, issues.size
185 assert_equal 1, issues.size
186 assert_equal 2, issues.first.id
186 assert_equal 2, issues.first.id
187 end
187 end
188
188
189 def test_operator_is_on_integer_custom_field_should_accept_negative_value
189 def test_operator_is_on_integer_custom_field_should_accept_negative_value
190 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
190 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
191 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
191 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
192 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
192 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
193 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
193 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
194
194
195 query = IssueQuery.new(:name => '_')
195 query = IssueQuery.new(:name => '_')
196 query.add_filter("cf_#{f.id}", '=', ['-12'])
196 query.add_filter("cf_#{f.id}", '=', ['-12'])
197 assert query.valid?
197 assert query.valid?
198 issues = find_issues_with_query(query)
198 issues = find_issues_with_query(query)
199 assert_equal 1, issues.size
199 assert_equal 1, issues.size
200 assert_equal 2, issues.first.id
200 assert_equal 2, issues.first.id
201 end
201 end
202
202
203 def test_operator_is_on_float_custom_field
203 def test_operator_is_on_float_custom_field
204 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
204 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
205 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
205 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
206 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
206 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
207 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
207 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
208
208
209 query = IssueQuery.new(:name => '_')
209 query = IssueQuery.new(:name => '_')
210 query.add_filter("cf_#{f.id}", '=', ['12.7'])
210 query.add_filter("cf_#{f.id}", '=', ['12.7'])
211 issues = find_issues_with_query(query)
211 issues = find_issues_with_query(query)
212 assert_equal 1, issues.size
212 assert_equal 1, issues.size
213 assert_equal 2, issues.first.id
213 assert_equal 2, issues.first.id
214 end
214 end
215
215
216 def test_operator_is_on_float_custom_field_should_accept_negative_value
216 def test_operator_is_on_float_custom_field_should_accept_negative_value
217 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
217 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
218 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
218 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
219 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
219 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
220 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
220 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
221
221
222 query = IssueQuery.new(:name => '_')
222 query = IssueQuery.new(:name => '_')
223 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
223 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
224 assert query.valid?
224 assert query.valid?
225 issues = find_issues_with_query(query)
225 issues = find_issues_with_query(query)
226 assert_equal 1, issues.size
226 assert_equal 1, issues.size
227 assert_equal 2, issues.first.id
227 assert_equal 2, issues.first.id
228 end
228 end
229
229
230 def test_operator_is_on_multi_list_custom_field
230 def test_operator_is_on_multi_list_custom_field
231 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
231 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
232 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
232 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
233 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
233 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
234 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
234 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
235 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
235 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
236
236
237 query = IssueQuery.new(:name => '_')
237 query = IssueQuery.new(:name => '_')
238 query.add_filter("cf_#{f.id}", '=', ['value1'])
238 query.add_filter("cf_#{f.id}", '=', ['value1'])
239 issues = find_issues_with_query(query)
239 issues = find_issues_with_query(query)
240 assert_equal [1, 3], issues.map(&:id).sort
240 assert_equal [1, 3], issues.map(&:id).sort
241
241
242 query = IssueQuery.new(:name => '_')
242 query = IssueQuery.new(:name => '_')
243 query.add_filter("cf_#{f.id}", '=', ['value2'])
243 query.add_filter("cf_#{f.id}", '=', ['value2'])
244 issues = find_issues_with_query(query)
244 issues = find_issues_with_query(query)
245 assert_equal [1], issues.map(&:id).sort
245 assert_equal [1], issues.map(&:id).sort
246 end
246 end
247
247
248 def test_operator_is_not_on_multi_list_custom_field
248 def test_operator_is_not_on_multi_list_custom_field
249 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
249 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
250 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
250 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
251 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
251 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
252 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
252 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
253 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
253 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
254
254
255 query = IssueQuery.new(:name => '_')
255 query = IssueQuery.new(:name => '_')
256 query.add_filter("cf_#{f.id}", '!', ['value1'])
256 query.add_filter("cf_#{f.id}", '!', ['value1'])
257 issues = find_issues_with_query(query)
257 issues = find_issues_with_query(query)
258 assert !issues.map(&:id).include?(1)
258 assert !issues.map(&:id).include?(1)
259 assert !issues.map(&:id).include?(3)
259 assert !issues.map(&:id).include?(3)
260
260
261 query = IssueQuery.new(:name => '_')
261 query = IssueQuery.new(:name => '_')
262 query.add_filter("cf_#{f.id}", '!', ['value2'])
262 query.add_filter("cf_#{f.id}", '!', ['value2'])
263 issues = find_issues_with_query(query)
263 issues = find_issues_with_query(query)
264 assert !issues.map(&:id).include?(1)
264 assert !issues.map(&:id).include?(1)
265 assert issues.map(&:id).include?(3)
265 assert issues.map(&:id).include?(3)
266 end
266 end
267
267
268 def test_operator_is_on_is_private_field
268 def test_operator_is_on_is_private_field
269 # is_private filter only available for those who can set issues private
269 # is_private filter only available for those who can set issues private
270 User.current = User.find(2)
270 User.current = User.find(2)
271
271
272 query = IssueQuery.new(:name => '_')
272 query = IssueQuery.new(:name => '_')
273 assert query.available_filters.key?('is_private')
273 assert query.available_filters.key?('is_private')
274
274
275 query.add_filter("is_private", '=', ['1'])
275 query.add_filter("is_private", '=', ['1'])
276 issues = find_issues_with_query(query)
276 issues = find_issues_with_query(query)
277 assert issues.any?
277 assert issues.any?
278 assert_nil issues.detect {|issue| !issue.is_private?}
278 assert_nil issues.detect {|issue| !issue.is_private?}
279 ensure
279 ensure
280 User.current = nil
280 User.current = nil
281 end
281 end
282
282
283 def test_operator_is_not_on_is_private_field
283 def test_operator_is_not_on_is_private_field
284 # is_private filter only available for those who can set issues private
284 # is_private filter only available for those who can set issues private
285 User.current = User.find(2)
285 User.current = User.find(2)
286
286
287 query = IssueQuery.new(:name => '_')
287 query = IssueQuery.new(:name => '_')
288 assert query.available_filters.key?('is_private')
288 assert query.available_filters.key?('is_private')
289
289
290 query.add_filter("is_private", '!', ['1'])
290 query.add_filter("is_private", '!', ['1'])
291 issues = find_issues_with_query(query)
291 issues = find_issues_with_query(query)
292 assert issues.any?
292 assert issues.any?
293 assert_nil issues.detect {|issue| issue.is_private?}
293 assert_nil issues.detect {|issue| issue.is_private?}
294 ensure
294 ensure
295 User.current = nil
295 User.current = nil
296 end
296 end
297
297
298 def test_operator_greater_than
298 def test_operator_greater_than
299 query = IssueQuery.new(:project => Project.find(1), :name => '_')
299 query = IssueQuery.new(:project => Project.find(1), :name => '_')
300 query.add_filter('done_ratio', '>=', ['40'])
300 query.add_filter('done_ratio', '>=', ['40'])
301 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
301 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
302 find_issues_with_query(query)
302 find_issues_with_query(query)
303 end
303 end
304
304
305 def test_operator_greater_than_a_float
305 def test_operator_greater_than_a_float
306 query = IssueQuery.new(:project => Project.find(1), :name => '_')
306 query = IssueQuery.new(:project => Project.find(1), :name => '_')
307 query.add_filter('estimated_hours', '>=', ['40.5'])
307 query.add_filter('estimated_hours', '>=', ['40.5'])
308 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
308 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
309 find_issues_with_query(query)
309 find_issues_with_query(query)
310 end
310 end
311
311
312 def test_operator_greater_than_on_int_custom_field
312 def test_operator_greater_than_on_int_custom_field
313 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
313 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
314 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
314 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
315 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
315 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
316 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
316 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
317
317
318 query = IssueQuery.new(:project => Project.find(1), :name => '_')
318 query = IssueQuery.new(:project => Project.find(1), :name => '_')
319 query.add_filter("cf_#{f.id}", '>=', ['8'])
319 query.add_filter("cf_#{f.id}", '>=', ['8'])
320 issues = find_issues_with_query(query)
320 issues = find_issues_with_query(query)
321 assert_equal 1, issues.size
321 assert_equal 1, issues.size
322 assert_equal 2, issues.first.id
322 assert_equal 2, issues.first.id
323 end
323 end
324
324
325 def test_operator_lesser_than
325 def test_operator_lesser_than
326 query = IssueQuery.new(:project => Project.find(1), :name => '_')
326 query = IssueQuery.new(:project => Project.find(1), :name => '_')
327 query.add_filter('done_ratio', '<=', ['30'])
327 query.add_filter('done_ratio', '<=', ['30'])
328 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
328 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
329 find_issues_with_query(query)
329 find_issues_with_query(query)
330 end
330 end
331
331
332 def test_operator_lesser_than_on_custom_field
332 def test_operator_lesser_than_on_custom_field
333 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
333 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
334 query = IssueQuery.new(:project => Project.find(1), :name => '_')
334 query = IssueQuery.new(:project => Project.find(1), :name => '_')
335 query.add_filter("cf_#{f.id}", '<=', ['30'])
335 query.add_filter("cf_#{f.id}", '<=', ['30'])
336 assert_match /CAST.+ <= 30\.0/, query.statement
336 assert_match /CAST.+ <= 30\.0/, query.statement
337 find_issues_with_query(query)
337 find_issues_with_query(query)
338 end
338 end
339
339
340 def test_operator_between
340 def test_operator_between
341 query = IssueQuery.new(:project => Project.find(1), :name => '_')
341 query = IssueQuery.new(:project => Project.find(1), :name => '_')
342 query.add_filter('done_ratio', '><', ['30', '40'])
342 query.add_filter('done_ratio', '><', ['30', '40'])
343 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
343 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
344 find_issues_with_query(query)
344 find_issues_with_query(query)
345 end
345 end
346
346
347 def test_operator_between_on_custom_field
347 def test_operator_between_on_custom_field
348 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
348 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
349 query = IssueQuery.new(:project => Project.find(1), :name => '_')
349 query = IssueQuery.new(:project => Project.find(1), :name => '_')
350 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
350 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
351 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
351 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
352 find_issues_with_query(query)
352 find_issues_with_query(query)
353 end
353 end
354
354
355 def test_date_filter_should_not_accept_non_date_values
355 def test_date_filter_should_not_accept_non_date_values
356 query = IssueQuery.new(:name => '_')
356 query = IssueQuery.new(:name => '_')
357 query.add_filter('created_on', '=', ['a'])
357 query.add_filter('created_on', '=', ['a'])
358
358
359 assert query.has_filter?('created_on')
359 assert query.has_filter?('created_on')
360 assert !query.valid?
360 assert !query.valid?
361 end
361 end
362
362
363 def test_date_filter_should_not_accept_invalid_date_values
363 def test_date_filter_should_not_accept_invalid_date_values
364 query = IssueQuery.new(:name => '_')
364 query = IssueQuery.new(:name => '_')
365 query.add_filter('created_on', '=', ['2011-01-34'])
365 query.add_filter('created_on', '=', ['2011-01-34'])
366
366
367 assert query.has_filter?('created_on')
367 assert query.has_filter?('created_on')
368 assert !query.valid?
368 assert !query.valid?
369 end
369 end
370
370
371 def test_relative_date_filter_should_not_accept_non_integer_values
371 def test_relative_date_filter_should_not_accept_non_integer_values
372 query = IssueQuery.new(:name => '_')
372 query = IssueQuery.new(:name => '_')
373 query.add_filter('created_on', '>t-', ['a'])
373 query.add_filter('created_on', '>t-', ['a'])
374
374
375 assert query.has_filter?('created_on')
375 assert query.has_filter?('created_on')
376 assert !query.valid?
376 assert !query.valid?
377 end
377 end
378
378
379 def test_operator_date_equals
379 def test_operator_date_equals
380 query = IssueQuery.new(:name => '_')
380 query = IssueQuery.new(:name => '_')
381 query.add_filter('due_date', '=', ['2011-07-10'])
381 query.add_filter('due_date', '=', ['2011-07-10'])
382 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
382 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
383 find_issues_with_query(query)
383 find_issues_with_query(query)
384 end
384 end
385
385
386 def test_operator_date_lesser_than
386 def test_operator_date_lesser_than
387 query = IssueQuery.new(:name => '_')
387 query = IssueQuery.new(:name => '_')
388 query.add_filter('due_date', '<=', ['2011-07-10'])
388 query.add_filter('due_date', '<=', ['2011-07-10'])
389 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
389 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
390 find_issues_with_query(query)
390 find_issues_with_query(query)
391 end
391 end
392
392
393 def test_operator_date_greater_than
393 def test_operator_date_greater_than
394 query = IssueQuery.new(:name => '_')
394 query = IssueQuery.new(:name => '_')
395 query.add_filter('due_date', '>=', ['2011-07-10'])
395 query.add_filter('due_date', '>=', ['2011-07-10'])
396 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
396 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
397 find_issues_with_query(query)
397 find_issues_with_query(query)
398 end
398 end
399
399
400 def test_operator_date_between
400 def test_operator_date_between
401 query = IssueQuery.new(:name => '_')
401 query = IssueQuery.new(:name => '_')
402 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
402 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
403 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
403 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
404 find_issues_with_query(query)
404 find_issues_with_query(query)
405 end
405 end
406
406
407 def test_operator_in_more_than
407 def test_operator_in_more_than
408 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
408 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
409 query = IssueQuery.new(:project => Project.find(1), :name => '_')
409 query = IssueQuery.new(:project => Project.find(1), :name => '_')
410 query.add_filter('due_date', '>t+', ['15'])
410 query.add_filter('due_date', '>t+', ['15'])
411 issues = find_issues_with_query(query)
411 issues = find_issues_with_query(query)
412 assert !issues.empty?
412 assert !issues.empty?
413 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
413 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
414 end
414 end
415
415
416 def test_operator_in_less_than
416 def test_operator_in_less_than
417 query = IssueQuery.new(:project => Project.find(1), :name => '_')
417 query = IssueQuery.new(:project => Project.find(1), :name => '_')
418 query.add_filter('due_date', '<t+', ['15'])
418 query.add_filter('due_date', '<t+', ['15'])
419 issues = find_issues_with_query(query)
419 issues = find_issues_with_query(query)
420 assert !issues.empty?
420 assert !issues.empty?
421 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
421 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
422 end
422 end
423
423
424 def test_operator_in_the_next_days
424 def test_operator_in_the_next_days
425 query = IssueQuery.new(:project => Project.find(1), :name => '_')
425 query = IssueQuery.new(:project => Project.find(1), :name => '_')
426 query.add_filter('due_date', '><t+', ['15'])
426 query.add_filter('due_date', '><t+', ['15'])
427 issues = find_issues_with_query(query)
427 issues = find_issues_with_query(query)
428 assert !issues.empty?
428 assert !issues.empty?
429 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
429 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
430 end
430 end
431
431
432 def test_operator_less_than_ago
432 def test_operator_less_than_ago
433 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
433 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
434 query = IssueQuery.new(:project => Project.find(1), :name => '_')
434 query = IssueQuery.new(:project => Project.find(1), :name => '_')
435 query.add_filter('due_date', '>t-', ['3'])
435 query.add_filter('due_date', '>t-', ['3'])
436 issues = find_issues_with_query(query)
436 issues = find_issues_with_query(query)
437 assert !issues.empty?
437 assert !issues.empty?
438 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
438 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
439 end
439 end
440
440
441 def test_operator_in_the_past_days
441 def test_operator_in_the_past_days
442 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
442 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
443 query = IssueQuery.new(:project => Project.find(1), :name => '_')
443 query = IssueQuery.new(:project => Project.find(1), :name => '_')
444 query.add_filter('due_date', '><t-', ['3'])
444 query.add_filter('due_date', '><t-', ['3'])
445 issues = find_issues_with_query(query)
445 issues = find_issues_with_query(query)
446 assert !issues.empty?
446 assert !issues.empty?
447 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
447 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
448 end
448 end
449
449
450 def test_operator_more_than_ago
450 def test_operator_more_than_ago
451 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
451 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
452 query = IssueQuery.new(:project => Project.find(1), :name => '_')
452 query = IssueQuery.new(:project => Project.find(1), :name => '_')
453 query.add_filter('due_date', '<t-', ['10'])
453 query.add_filter('due_date', '<t-', ['10'])
454 assert query.statement.include?("#{Issue.table_name}.due_date <=")
454 assert query.statement.include?("#{Issue.table_name}.due_date <=")
455 issues = find_issues_with_query(query)
455 issues = find_issues_with_query(query)
456 assert !issues.empty?
456 assert !issues.empty?
457 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
457 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
458 end
458 end
459
459
460 def test_operator_in
460 def test_operator_in
461 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
461 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
462 query = IssueQuery.new(:project => Project.find(1), :name => '_')
462 query = IssueQuery.new(:project => Project.find(1), :name => '_')
463 query.add_filter('due_date', 't+', ['2'])
463 query.add_filter('due_date', 't+', ['2'])
464 issues = find_issues_with_query(query)
464 issues = find_issues_with_query(query)
465 assert !issues.empty?
465 assert !issues.empty?
466 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
466 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
467 end
467 end
468
468
469 def test_operator_ago
469 def test_operator_ago
470 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
470 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
471 query = IssueQuery.new(:project => Project.find(1), :name => '_')
471 query = IssueQuery.new(:project => Project.find(1), :name => '_')
472 query.add_filter('due_date', 't-', ['3'])
472 query.add_filter('due_date', 't-', ['3'])
473 issues = find_issues_with_query(query)
473 issues = find_issues_with_query(query)
474 assert !issues.empty?
474 assert !issues.empty?
475 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
475 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
476 end
476 end
477
477
478 def test_operator_today
478 def test_operator_today
479 query = IssueQuery.new(:project => Project.find(1), :name => '_')
479 query = IssueQuery.new(:project => Project.find(1), :name => '_')
480 query.add_filter('due_date', 't', [''])
480 query.add_filter('due_date', 't', [''])
481 issues = find_issues_with_query(query)
481 issues = find_issues_with_query(query)
482 assert !issues.empty?
482 assert !issues.empty?
483 issues.each {|issue| assert_equal Date.today, issue.due_date}
483 issues.each {|issue| assert_equal Date.today, issue.due_date}
484 end
484 end
485
485
486 def test_operator_this_week_on_date
486 def test_operator_this_week_on_date
487 query = IssueQuery.new(:project => Project.find(1), :name => '_')
487 query = IssueQuery.new(:project => Project.find(1), :name => '_')
488 query.add_filter('due_date', 'w', [''])
488 query.add_filter('due_date', 'w', [''])
489 find_issues_with_query(query)
489 find_issues_with_query(query)
490 end
490 end
491
491
492 def test_operator_this_week_on_datetime
492 def test_operator_this_week_on_datetime
493 query = IssueQuery.new(:project => Project.find(1), :name => '_')
493 query = IssueQuery.new(:project => Project.find(1), :name => '_')
494 query.add_filter('created_on', 'w', [''])
494 query.add_filter('created_on', 'w', [''])
495 find_issues_with_query(query)
495 find_issues_with_query(query)
496 end
496 end
497
497
498 def test_operator_contains
498 def test_operator_contains
499 query = IssueQuery.new(:project => Project.find(1), :name => '_')
499 query = IssueQuery.new(:project => Project.find(1), :name => '_')
500 query.add_filter('subject', '~', ['uNable'])
500 query.add_filter('subject', '~', ['uNable'])
501 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
501 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
502 result = find_issues_with_query(query)
502 result = find_issues_with_query(query)
503 assert result.empty?
503 assert result.empty?
504 result.each {|issue| assert issue.subject.downcase.include?('unable') }
504 result.each {|issue| assert issue.subject.downcase.include?('unable') }
505 end
505 end
506
506
507 def test_range_for_this_week_with_week_starting_on_monday
507 def test_range_for_this_week_with_week_starting_on_monday
508 I18n.locale = :fr
508 I18n.locale = :fr
509 assert_equal '1', I18n.t(:general_first_day_of_week)
509 assert_equal '1', I18n.t(:general_first_day_of_week)
510
510
511 Date.stubs(:today).returns(Date.parse('2011-04-29'))
511 Date.stubs(:today).returns(Date.parse('2011-04-29'))
512
512
513 query = IssueQuery.new(:project => Project.find(1), :name => '_')
513 query = IssueQuery.new(:project => Project.find(1), :name => '_')
514 query.add_filter('due_date', 'w', [''])
514 query.add_filter('due_date', 'w', [''])
515 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
515 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
516 I18n.locale = :en
516 I18n.locale = :en
517 end
517 end
518
518
519 def test_range_for_this_week_with_week_starting_on_sunday
519 def test_range_for_this_week_with_week_starting_on_sunday
520 I18n.locale = :en
520 I18n.locale = :en
521 assert_equal '7', I18n.t(:general_first_day_of_week)
521 assert_equal '7', I18n.t(:general_first_day_of_week)
522
522
523 Date.stubs(:today).returns(Date.parse('2011-04-29'))
523 Date.stubs(:today).returns(Date.parse('2011-04-29'))
524
524
525 query = IssueQuery.new(:project => Project.find(1), :name => '_')
525 query = IssueQuery.new(:project => Project.find(1), :name => '_')
526 query.add_filter('due_date', 'w', [''])
526 query.add_filter('due_date', 'w', [''])
527 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
527 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
528 end
528 end
529
529
530 def test_operator_does_not_contains
530 def test_operator_does_not_contains
531 query = IssueQuery.new(:project => Project.find(1), :name => '_')
531 query = IssueQuery.new(:project => Project.find(1), :name => '_')
532 query.add_filter('subject', '!~', ['uNable'])
532 query.add_filter('subject', '!~', ['uNable'])
533 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
533 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
534 find_issues_with_query(query)
534 find_issues_with_query(query)
535 end
535 end
536
536
537 def test_filter_assigned_to_me
537 def test_filter_assigned_to_me
538 user = User.find(2)
538 user = User.find(2)
539 group = Group.find(10)
539 group = Group.find(10)
540 User.current = user
540 User.current = user
541 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
541 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
542 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
542 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
543 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
543 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
544 group.users << user
544 group.users << user
545
545
546 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
546 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
547 result = query.issues
547 result = query.issues
548 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
548 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
549
549
550 assert result.include?(i1)
550 assert result.include?(i1)
551 assert result.include?(i2)
551 assert result.include?(i2)
552 assert !result.include?(i3)
552 assert !result.include?(i3)
553 end
553 end
554
554
555 def test_user_custom_field_filtered_on_me
555 def test_user_custom_field_filtered_on_me
556 User.current = User.find(2)
556 User.current = User.find(2)
557 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
557 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
558 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
558 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
559 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
559 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
560
560
561 query = IssueQuery.new(:name => '_', :project => Project.find(1))
561 query = IssueQuery.new(:name => '_', :project => Project.find(1))
562 filter = query.available_filters["cf_#{cf.id}"]
562 filter = query.available_filters["cf_#{cf.id}"]
563 assert_not_nil filter
563 assert_not_nil filter
564 assert_include 'me', filter[:values].map{|v| v[1]}
564 assert_include 'me', filter[:values].map{|v| v[1]}
565
565
566 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
566 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
567 result = query.issues
567 result = query.issues
568 assert_equal 1, result.size
568 assert_equal 1, result.size
569 assert_equal issue1, result.first
569 assert_equal issue1, result.first
570 end
570 end
571
571
572 def test_filter_my_projects
572 def test_filter_my_projects
573 User.current = User.find(2)
573 User.current = User.find(2)
574 query = IssueQuery.new(:name => '_')
574 query = IssueQuery.new(:name => '_')
575 filter = query.available_filters['project_id']
575 filter = query.available_filters['project_id']
576 assert_not_nil filter
576 assert_not_nil filter
577 assert_include 'mine', filter[:values].map{|v| v[1]}
577 assert_include 'mine', filter[:values].map{|v| v[1]}
578
578
579 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
579 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
580 result = query.issues
580 result = query.issues
581 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
581 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
582 end
582 end
583
583
584 def test_filter_watched_issues
584 def test_filter_watched_issues
585 User.current = User.find(1)
585 User.current = User.find(1)
586 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
586 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
587 result = find_issues_with_query(query)
587 result = find_issues_with_query(query)
588 assert_not_nil result
588 assert_not_nil result
589 assert !result.empty?
589 assert !result.empty?
590 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
590 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
591 User.current = nil
591 User.current = nil
592 end
592 end
593
593
594 def test_filter_unwatched_issues
594 def test_filter_unwatched_issues
595 User.current = User.find(1)
595 User.current = User.find(1)
596 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
596 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
597 result = find_issues_with_query(query)
597 result = find_issues_with_query(query)
598 assert_not_nil result
598 assert_not_nil result
599 assert !result.empty?
599 assert !result.empty?
600 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
600 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
601 User.current = nil
601 User.current = nil
602 end
602 end
603
603
604 def test_filter_on_project_custom_field
604 def test_filter_on_project_custom_field
605 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
605 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
606 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
606 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
607 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
607 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
608
608
609 query = IssueQuery.new(:name => '_')
609 query = IssueQuery.new(:name => '_')
610 filter_name = "project.cf_#{field.id}"
610 filter_name = "project.cf_#{field.id}"
611 assert_include filter_name, query.available_filters.keys
611 assert_include filter_name, query.available_filters.keys
612 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
612 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
613 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
613 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
614 end
614 end
615
615
616 def test_filter_on_author_custom_field
616 def test_filter_on_author_custom_field
617 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
617 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
618 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
618 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
619
619
620 query = IssueQuery.new(:name => '_')
620 query = IssueQuery.new(:name => '_')
621 filter_name = "author.cf_#{field.id}"
621 filter_name = "author.cf_#{field.id}"
622 assert_include filter_name, query.available_filters.keys
622 assert_include filter_name, query.available_filters.keys
623 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
623 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
624 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
624 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
625 end
625 end
626
626
627 def test_filter_on_assigned_to_custom_field
627 def test_filter_on_assigned_to_custom_field
628 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
628 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
629 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
629 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
630
630
631 query = IssueQuery.new(:name => '_')
631 query = IssueQuery.new(:name => '_')
632 filter_name = "assigned_to.cf_#{field.id}"
632 filter_name = "assigned_to.cf_#{field.id}"
633 assert_include filter_name, query.available_filters.keys
633 assert_include filter_name, query.available_filters.keys
634 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
634 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
635 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
635 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
636 end
636 end
637
637
638 def test_filter_on_fixed_version_custom_field
638 def test_filter_on_fixed_version_custom_field
639 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
639 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
640 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
640 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
641
641
642 query = IssueQuery.new(:name => '_')
642 query = IssueQuery.new(:name => '_')
643 filter_name = "fixed_version.cf_#{field.id}"
643 filter_name = "fixed_version.cf_#{field.id}"
644 assert_include filter_name, query.available_filters.keys
644 assert_include filter_name, query.available_filters.keys
645 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
645 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
646 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
646 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
647 end
647 end
648
648
649 def test_filter_on_relations_with_a_specific_issue
649 def test_filter_on_relations_with_a_specific_issue
650 IssueRelation.delete_all
650 IssueRelation.delete_all
651 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
651 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
652 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
652 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
653
653
654 query = IssueQuery.new(:name => '_')
654 query = IssueQuery.new(:name => '_')
655 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
655 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
656 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
656 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
657
657
658 query = IssueQuery.new(:name => '_')
658 query = IssueQuery.new(:name => '_')
659 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
659 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
660 assert_equal [1], find_issues_with_query(query).map(&:id).sort
660 assert_equal [1], find_issues_with_query(query).map(&:id).sort
661 end
661 end
662
662
663 def test_filter_on_relations_with_any_issues_in_a_project
663 def test_filter_on_relations_with_any_issues_in_a_project
664 IssueRelation.delete_all
664 IssueRelation.delete_all
665 with_settings :cross_project_issue_relations => '1' do
665 with_settings :cross_project_issue_relations => '1' do
666 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
666 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
667 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
667 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
668 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
668 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
669 end
669 end
670
670
671 query = IssueQuery.new(:name => '_')
671 query = IssueQuery.new(:name => '_')
672 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
672 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
673 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
673 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
674
674
675 query = IssueQuery.new(:name => '_')
675 query = IssueQuery.new(:name => '_')
676 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
676 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
677 assert_equal [1], find_issues_with_query(query).map(&:id).sort
677 assert_equal [1], find_issues_with_query(query).map(&:id).sort
678
678
679 query = IssueQuery.new(:name => '_')
679 query = IssueQuery.new(:name => '_')
680 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
680 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
681 assert_equal [], find_issues_with_query(query).map(&:id).sort
681 assert_equal [], find_issues_with_query(query).map(&:id).sort
682 end
682 end
683
683
684 def test_filter_on_relations_with_any_issues_not_in_a_project
684 def test_filter_on_relations_with_any_issues_not_in_a_project
685 IssueRelation.delete_all
685 IssueRelation.delete_all
686 with_settings :cross_project_issue_relations => '1' do
686 with_settings :cross_project_issue_relations => '1' do
687 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
687 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
688 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
688 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
689 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
689 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
690 end
690 end
691
691
692 query = IssueQuery.new(:name => '_')
692 query = IssueQuery.new(:name => '_')
693 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
693 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
694 assert_equal [1], find_issues_with_query(query).map(&:id).sort
694 assert_equal [1], find_issues_with_query(query).map(&:id).sort
695 end
695 end
696
696
697 def test_filter_on_relations_with_no_issues_in_a_project
697 def test_filter_on_relations_with_no_issues_in_a_project
698 IssueRelation.delete_all
698 IssueRelation.delete_all
699 with_settings :cross_project_issue_relations => '1' do
699 with_settings :cross_project_issue_relations => '1' do
700 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
700 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
701 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
701 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
702 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
702 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
703 end
703 end
704
704
705 query = IssueQuery.new(:name => '_')
705 query = IssueQuery.new(:name => '_')
706 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
706 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
707 ids = find_issues_with_query(query).map(&:id).sort
707 ids = find_issues_with_query(query).map(&:id).sort
708 assert_include 2, ids
708 assert_include 2, ids
709 assert_not_include 1, ids
709 assert_not_include 1, ids
710 assert_not_include 3, ids
710 assert_not_include 3, ids
711 end
711 end
712
712
713 def test_filter_on_relations_with_no_issues
713 def test_filter_on_relations_with_no_issues
714 IssueRelation.delete_all
714 IssueRelation.delete_all
715 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
715 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
716 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
716 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
717
717
718 query = IssueQuery.new(:name => '_')
718 query = IssueQuery.new(:name => '_')
719 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
719 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
720 ids = find_issues_with_query(query).map(&:id)
720 ids = find_issues_with_query(query).map(&:id)
721 assert_equal [], ids & [1, 2, 3]
721 assert_equal [], ids & [1, 2, 3]
722 assert_include 4, ids
722 assert_include 4, ids
723 end
723 end
724
724
725 def test_filter_on_relations_with_any_issues
725 def test_filter_on_relations_with_any_issues
726 IssueRelation.delete_all
726 IssueRelation.delete_all
727 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
727 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
728 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
728 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
729
729
730 query = IssueQuery.new(:name => '_')
730 query = IssueQuery.new(:name => '_')
731 query.filters = {"relates" => {:operator => '*', :values => ['']}}
731 query.filters = {"relates" => {:operator => '*', :values => ['']}}
732 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
732 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
733 end
733 end
734
734
735 def test_statement_should_be_nil_with_no_filters
735 def test_statement_should_be_nil_with_no_filters
736 q = IssueQuery.new(:name => '_')
736 q = IssueQuery.new(:name => '_')
737 q.filters = {}
737 q.filters = {}
738
738
739 assert q.valid?
739 assert q.valid?
740 assert_nil q.statement
740 assert_nil q.statement
741 end
741 end
742
742
743 def test_default_columns
743 def test_default_columns
744 q = IssueQuery.new
744 q = IssueQuery.new
745 assert q.columns.any?
745 assert q.columns.any?
746 assert q.inline_columns.any?
746 assert q.inline_columns.any?
747 assert q.block_columns.empty?
747 assert q.block_columns.empty?
748 end
748 end
749
749
750 def test_set_column_names
750 def test_set_column_names
751 q = IssueQuery.new
751 q = IssueQuery.new
752 q.column_names = ['tracker', :subject, '', 'unknonw_column']
752 q.column_names = ['tracker', :subject, '', 'unknonw_column']
753 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
753 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
754 c = q.columns.first
754 end
755 assert q.has_column?(c)
755
756 def test_has_column_should_accept_a_column_name
757 q = IssueQuery.new
758 q.column_names = ['tracker', :subject]
759 assert q.has_column?(:tracker)
760 assert !q.has_column?(:category)
761 end
762
763 def test_has_column_should_accept_a_column
764 q = IssueQuery.new
765 q.column_names = ['tracker', :subject]
766
767 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
768 assert_kind_of QueryColumn, tracker_column
769 category_column = q.available_columns.detect {|c| c.name==:category}
770 assert_kind_of QueryColumn, category_column
771
772 assert q.has_column?(tracker_column)
773 assert !q.has_column?(category_column)
756 end
774 end
757
775
758 def test_inline_and_block_columns
776 def test_inline_and_block_columns
759 q = IssueQuery.new
777 q = IssueQuery.new
760 q.column_names = ['subject', 'description', 'tracker']
778 q.column_names = ['subject', 'description', 'tracker']
761
779
762 assert_equal [:subject, :tracker], q.inline_columns.map(&:name)
780 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
763 assert_equal [:description], q.block_columns.map(&:name)
781 assert_equal [:description], q.block_columns.map(&:name)
764 end
782 end
765
783
766 def test_custom_field_columns_should_be_inline
784 def test_custom_field_columns_should_be_inline
767 q = IssueQuery.new
785 q = IssueQuery.new
768 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
786 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
769 assert columns.any?
787 assert columns.any?
770 assert_nil columns.detect {|column| !column.inline?}
788 assert_nil columns.detect {|column| !column.inline?}
771 end
789 end
772
790
773 def test_query_should_preload_spent_hours
791 def test_query_should_preload_spent_hours
774 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
792 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
775 assert q.has_column?(:spent_hours)
793 assert q.has_column?(:spent_hours)
776 issues = q.issues
794 issues = q.issues
777 assert_not_nil issues.first.instance_variable_get("@spent_hours")
795 assert_not_nil issues.first.instance_variable_get("@spent_hours")
778 end
796 end
779
797
780 def test_groupable_columns_should_include_custom_fields
798 def test_groupable_columns_should_include_custom_fields
781 q = IssueQuery.new
799 q = IssueQuery.new
782 column = q.groupable_columns.detect {|c| c.name == :cf_1}
800 column = q.groupable_columns.detect {|c| c.name == :cf_1}
783 assert_not_nil column
801 assert_not_nil column
784 assert_kind_of QueryCustomFieldColumn, column
802 assert_kind_of QueryCustomFieldColumn, column
785 end
803 end
786
804
787 def test_groupable_columns_should_not_include_multi_custom_fields
805 def test_groupable_columns_should_not_include_multi_custom_fields
788 field = CustomField.find(1)
806 field = CustomField.find(1)
789 field.update_attribute :multiple, true
807 field.update_attribute :multiple, true
790
808
791 q = IssueQuery.new
809 q = IssueQuery.new
792 column = q.groupable_columns.detect {|c| c.name == :cf_1}
810 column = q.groupable_columns.detect {|c| c.name == :cf_1}
793 assert_nil column
811 assert_nil column
794 end
812 end
795
813
796 def test_groupable_columns_should_include_user_custom_fields
814 def test_groupable_columns_should_include_user_custom_fields
797 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
815 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
798
816
799 q = IssueQuery.new
817 q = IssueQuery.new
800 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
818 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
801 end
819 end
802
820
803 def test_groupable_columns_should_include_version_custom_fields
821 def test_groupable_columns_should_include_version_custom_fields
804 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
822 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
805
823
806 q = IssueQuery.new
824 q = IssueQuery.new
807 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
825 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
808 end
826 end
809
827
810 def test_grouped_with_valid_column
828 def test_grouped_with_valid_column
811 q = IssueQuery.new(:group_by => 'status')
829 q = IssueQuery.new(:group_by => 'status')
812 assert q.grouped?
830 assert q.grouped?
813 assert_not_nil q.group_by_column
831 assert_not_nil q.group_by_column
814 assert_equal :status, q.group_by_column.name
832 assert_equal :status, q.group_by_column.name
815 assert_not_nil q.group_by_statement
833 assert_not_nil q.group_by_statement
816 assert_equal 'status', q.group_by_statement
834 assert_equal 'status', q.group_by_statement
817 end
835 end
818
836
819 def test_grouped_with_invalid_column
837 def test_grouped_with_invalid_column
820 q = IssueQuery.new(:group_by => 'foo')
838 q = IssueQuery.new(:group_by => 'foo')
821 assert !q.grouped?
839 assert !q.grouped?
822 assert_nil q.group_by_column
840 assert_nil q.group_by_column
823 assert_nil q.group_by_statement
841 assert_nil q.group_by_statement
824 end
842 end
825
843
826 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
844 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
827 with_settings :user_format => 'lastname_coma_firstname' do
845 with_settings :user_format => 'lastname_coma_firstname' do
828 q = IssueQuery.new
846 q = IssueQuery.new
829 assert q.sortable_columns.has_key?('assigned_to')
847 assert q.sortable_columns.has_key?('assigned_to')
830 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
848 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
831 end
849 end
832 end
850 end
833
851
834 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
852 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
835 with_settings :user_format => 'lastname_coma_firstname' do
853 with_settings :user_format => 'lastname_coma_firstname' do
836 q = IssueQuery.new
854 q = IssueQuery.new
837 assert q.sortable_columns.has_key?('author')
855 assert q.sortable_columns.has_key?('author')
838 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
856 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
839 end
857 end
840 end
858 end
841
859
842 def test_sortable_columns_should_include_custom_field
860 def test_sortable_columns_should_include_custom_field
843 q = IssueQuery.new
861 q = IssueQuery.new
844 assert q.sortable_columns['cf_1']
862 assert q.sortable_columns['cf_1']
845 end
863 end
846
864
847 def test_sortable_columns_should_not_include_multi_custom_field
865 def test_sortable_columns_should_not_include_multi_custom_field
848 field = CustomField.find(1)
866 field = CustomField.find(1)
849 field.update_attribute :multiple, true
867 field.update_attribute :multiple, true
850
868
851 q = IssueQuery.new
869 q = IssueQuery.new
852 assert !q.sortable_columns['cf_1']
870 assert !q.sortable_columns['cf_1']
853 end
871 end
854
872
855 def test_default_sort
873 def test_default_sort
856 q = IssueQuery.new
874 q = IssueQuery.new
857 assert_equal [], q.sort_criteria
875 assert_equal [], q.sort_criteria
858 end
876 end
859
877
860 def test_set_sort_criteria_with_hash
878 def test_set_sort_criteria_with_hash
861 q = IssueQuery.new
879 q = IssueQuery.new
862 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
880 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
863 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
881 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
864 end
882 end
865
883
866 def test_set_sort_criteria_with_array
884 def test_set_sort_criteria_with_array
867 q = IssueQuery.new
885 q = IssueQuery.new
868 q.sort_criteria = [['priority', 'desc'], 'tracker']
886 q.sort_criteria = [['priority', 'desc'], 'tracker']
869 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
887 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
870 end
888 end
871
889
872 def test_create_query_with_sort
890 def test_create_query_with_sort
873 q = IssueQuery.new(:name => 'Sorted')
891 q = IssueQuery.new(:name => 'Sorted')
874 q.sort_criteria = [['priority', 'desc'], 'tracker']
892 q.sort_criteria = [['priority', 'desc'], 'tracker']
875 assert q.save
893 assert q.save
876 q.reload
894 q.reload
877 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
895 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
878 end
896 end
879
897
880 def test_sort_by_string_custom_field_asc
898 def test_sort_by_string_custom_field_asc
881 q = IssueQuery.new
899 q = IssueQuery.new
882 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
900 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
883 assert c
901 assert c
884 assert c.sortable
902 assert c.sortable
885 issues = q.issues(:order => "#{c.sortable} ASC")
903 issues = q.issues(:order => "#{c.sortable} ASC")
886 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
904 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
887 assert !values.empty?
905 assert !values.empty?
888 assert_equal values.sort, values
906 assert_equal values.sort, values
889 end
907 end
890
908
891 def test_sort_by_string_custom_field_desc
909 def test_sort_by_string_custom_field_desc
892 q = IssueQuery.new
910 q = IssueQuery.new
893 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
911 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
894 assert c
912 assert c
895 assert c.sortable
913 assert c.sortable
896 issues = q.issues(:order => "#{c.sortable} DESC")
914 issues = q.issues(:order => "#{c.sortable} DESC")
897 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
915 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
898 assert !values.empty?
916 assert !values.empty?
899 assert_equal values.sort.reverse, values
917 assert_equal values.sort.reverse, values
900 end
918 end
901
919
902 def test_sort_by_float_custom_field_asc
920 def test_sort_by_float_custom_field_asc
903 q = IssueQuery.new
921 q = IssueQuery.new
904 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
922 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
905 assert c
923 assert c
906 assert c.sortable
924 assert c.sortable
907 issues = q.issues(:order => "#{c.sortable} ASC")
925 issues = q.issues(:order => "#{c.sortable} ASC")
908 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
926 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
909 assert !values.empty?
927 assert !values.empty?
910 assert_equal values.sort, values
928 assert_equal values.sort, values
911 end
929 end
912
930
913 def test_invalid_query_should_raise_query_statement_invalid_error
931 def test_invalid_query_should_raise_query_statement_invalid_error
914 q = IssueQuery.new
932 q = IssueQuery.new
915 assert_raise Query::StatementInvalid do
933 assert_raise Query::StatementInvalid do
916 q.issues(:conditions => "foo = 1")
934 q.issues(:conditions => "foo = 1")
917 end
935 end
918 end
936 end
919
937
920 def test_issue_count
938 def test_issue_count
921 q = IssueQuery.new(:name => '_')
939 q = IssueQuery.new(:name => '_')
922 issue_count = q.issue_count
940 issue_count = q.issue_count
923 assert_equal q.issues.size, issue_count
941 assert_equal q.issues.size, issue_count
924 end
942 end
925
943
926 def test_issue_count_with_archived_issues
944 def test_issue_count_with_archived_issues
927 p = Project.generate! do |project|
945 p = Project.generate! do |project|
928 project.status = Project::STATUS_ARCHIVED
946 project.status = Project::STATUS_ARCHIVED
929 end
947 end
930 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
948 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
931 assert !i.visible?
949 assert !i.visible?
932
950
933 test_issue_count
951 test_issue_count
934 end
952 end
935
953
936 def test_issue_count_by_association_group
954 def test_issue_count_by_association_group
937 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
955 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
938 count_by_group = q.issue_count_by_group
956 count_by_group = q.issue_count_by_group
939 assert_kind_of Hash, count_by_group
957 assert_kind_of Hash, count_by_group
940 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
958 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
941 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
959 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
942 assert count_by_group.has_key?(User.find(3))
960 assert count_by_group.has_key?(User.find(3))
943 end
961 end
944
962
945 def test_issue_count_by_list_custom_field_group
963 def test_issue_count_by_list_custom_field_group
946 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
964 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
947 count_by_group = q.issue_count_by_group
965 count_by_group = q.issue_count_by_group
948 assert_kind_of Hash, count_by_group
966 assert_kind_of Hash, count_by_group
949 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
967 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
950 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
968 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
951 assert count_by_group.has_key?('MySQL')
969 assert count_by_group.has_key?('MySQL')
952 end
970 end
953
971
954 def test_issue_count_by_date_custom_field_group
972 def test_issue_count_by_date_custom_field_group
955 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
973 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
956 count_by_group = q.issue_count_by_group
974 count_by_group = q.issue_count_by_group
957 assert_kind_of Hash, count_by_group
975 assert_kind_of Hash, count_by_group
958 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
976 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
959 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
977 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
960 end
978 end
961
979
962 def test_issue_count_with_nil_group_only
980 def test_issue_count_with_nil_group_only
963 Issue.update_all("assigned_to_id = NULL")
981 Issue.update_all("assigned_to_id = NULL")
964
982
965 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
983 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
966 count_by_group = q.issue_count_by_group
984 count_by_group = q.issue_count_by_group
967 assert_kind_of Hash, count_by_group
985 assert_kind_of Hash, count_by_group
968 assert_equal 1, count_by_group.keys.size
986 assert_equal 1, count_by_group.keys.size
969 assert_nil count_by_group.keys.first
987 assert_nil count_by_group.keys.first
970 end
988 end
971
989
972 def test_issue_ids
990 def test_issue_ids
973 q = IssueQuery.new(:name => '_')
991 q = IssueQuery.new(:name => '_')
974 order = "issues.subject, issues.id"
992 order = "issues.subject, issues.id"
975 issues = q.issues(:order => order)
993 issues = q.issues(:order => order)
976 assert_equal issues.map(&:id), q.issue_ids(:order => order)
994 assert_equal issues.map(&:id), q.issue_ids(:order => order)
977 end
995 end
978
996
979 def test_label_for
997 def test_label_for
980 set_language_if_valid 'en'
998 set_language_if_valid 'en'
981 q = IssueQuery.new
999 q = IssueQuery.new
982 assert_equal 'Assignee', q.label_for('assigned_to_id')
1000 assert_equal 'Assignee', q.label_for('assigned_to_id')
983 end
1001 end
984
1002
985 def test_label_for_fr
1003 def test_label_for_fr
986 set_language_if_valid 'fr'
1004 set_language_if_valid 'fr'
987 q = IssueQuery.new
1005 q = IssueQuery.new
988 s = "Assign\xc3\xa9 \xc3\xa0"
1006 s = "Assign\xc3\xa9 \xc3\xa0"
989 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
1007 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
990 assert_equal s, q.label_for('assigned_to_id')
1008 assert_equal s, q.label_for('assigned_to_id')
991 end
1009 end
992
1010
993 def test_editable_by
1011 def test_editable_by
994 admin = User.find(1)
1012 admin = User.find(1)
995 manager = User.find(2)
1013 manager = User.find(2)
996 developer = User.find(3)
1014 developer = User.find(3)
997
1015
998 # Public query on project 1
1016 # Public query on project 1
999 q = IssueQuery.find(1)
1017 q = IssueQuery.find(1)
1000 assert q.editable_by?(admin)
1018 assert q.editable_by?(admin)
1001 assert q.editable_by?(manager)
1019 assert q.editable_by?(manager)
1002 assert !q.editable_by?(developer)
1020 assert !q.editable_by?(developer)
1003
1021
1004 # Private query on project 1
1022 # Private query on project 1
1005 q = IssueQuery.find(2)
1023 q = IssueQuery.find(2)
1006 assert q.editable_by?(admin)
1024 assert q.editable_by?(admin)
1007 assert !q.editable_by?(manager)
1025 assert !q.editable_by?(manager)
1008 assert q.editable_by?(developer)
1026 assert q.editable_by?(developer)
1009
1027
1010 # Private query for all projects
1028 # Private query for all projects
1011 q = IssueQuery.find(3)
1029 q = IssueQuery.find(3)
1012 assert q.editable_by?(admin)
1030 assert q.editable_by?(admin)
1013 assert !q.editable_by?(manager)
1031 assert !q.editable_by?(manager)
1014 assert q.editable_by?(developer)
1032 assert q.editable_by?(developer)
1015
1033
1016 # Public query for all projects
1034 # Public query for all projects
1017 q = IssueQuery.find(4)
1035 q = IssueQuery.find(4)
1018 assert q.editable_by?(admin)
1036 assert q.editable_by?(admin)
1019 assert !q.editable_by?(manager)
1037 assert !q.editable_by?(manager)
1020 assert !q.editable_by?(developer)
1038 assert !q.editable_by?(developer)
1021 end
1039 end
1022
1040
1023 def test_visible_scope
1041 def test_visible_scope
1024 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1042 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1025
1043
1026 assert query_ids.include?(1), 'public query on public project was not visible'
1044 assert query_ids.include?(1), 'public query on public project was not visible'
1027 assert query_ids.include?(4), 'public query for all projects was not visible'
1045 assert query_ids.include?(4), 'public query for all projects was not visible'
1028 assert !query_ids.include?(2), 'private query on public project was visible'
1046 assert !query_ids.include?(2), 'private query on public project was visible'
1029 assert !query_ids.include?(3), 'private query for all projects was visible'
1047 assert !query_ids.include?(3), 'private query for all projects was visible'
1030 assert !query_ids.include?(7), 'public query on private project was visible'
1048 assert !query_ids.include?(7), 'public query on private project was visible'
1031 end
1049 end
1032
1050
1033 test "#available_filters should include users of visible projects in cross-project view" do
1051 test "#available_filters should include users of visible projects in cross-project view" do
1034 users = IssueQuery.new.available_filters["assigned_to_id"]
1052 users = IssueQuery.new.available_filters["assigned_to_id"]
1035 assert_not_nil users
1053 assert_not_nil users
1036 assert users[:values].map{|u|u[1]}.include?("3")
1054 assert users[:values].map{|u|u[1]}.include?("3")
1037 end
1055 end
1038
1056
1039 test "#available_filters should include users of subprojects" do
1057 test "#available_filters should include users of subprojects" do
1040 user1 = User.generate!
1058 user1 = User.generate!
1041 user2 = User.generate!
1059 user2 = User.generate!
1042 project = Project.find(1)
1060 project = Project.find(1)
1043 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1061 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1044
1062
1045 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1063 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1046 assert_not_nil users
1064 assert_not_nil users
1047 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1065 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1048 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1066 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1049 end
1067 end
1050
1068
1051 test "#available_filters should include visible projects in cross-project view" do
1069 test "#available_filters should include visible projects in cross-project view" do
1052 projects = IssueQuery.new.available_filters["project_id"]
1070 projects = IssueQuery.new.available_filters["project_id"]
1053 assert_not_nil projects
1071 assert_not_nil projects
1054 assert projects[:values].map{|u|u[1]}.include?("1")
1072 assert projects[:values].map{|u|u[1]}.include?("1")
1055 end
1073 end
1056
1074
1057 test "#available_filters should include 'member_of_group' filter" do
1075 test "#available_filters should include 'member_of_group' filter" do
1058 query = IssueQuery.new
1076 query = IssueQuery.new
1059 assert query.available_filters.keys.include?("member_of_group")
1077 assert query.available_filters.keys.include?("member_of_group")
1060 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1078 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1061 assert query.available_filters["member_of_group"][:values].present?
1079 assert query.available_filters["member_of_group"][:values].present?
1062 assert_equal Group.all.sort.map {|g| [g.name, g.id.to_s]},
1080 assert_equal Group.all.sort.map {|g| [g.name, g.id.to_s]},
1063 query.available_filters["member_of_group"][:values].sort
1081 query.available_filters["member_of_group"][:values].sort
1064 end
1082 end
1065
1083
1066 test "#available_filters should include 'assigned_to_role' filter" do
1084 test "#available_filters should include 'assigned_to_role' filter" do
1067 query = IssueQuery.new
1085 query = IssueQuery.new
1068 assert query.available_filters.keys.include?("assigned_to_role")
1086 assert query.available_filters.keys.include?("assigned_to_role")
1069 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1087 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1070
1088
1071 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1089 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1072 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1090 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1073 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1091 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1074
1092
1075 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1093 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1076 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1094 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1077 end
1095 end
1078
1096
1079 context "#statement" do
1097 context "#statement" do
1080 context "with 'member_of_group' filter" do
1098 context "with 'member_of_group' filter" do
1081 setup do
1099 setup do
1082 Group.destroy_all # No fixtures
1100 Group.destroy_all # No fixtures
1083 @user_in_group = User.generate!
1101 @user_in_group = User.generate!
1084 @second_user_in_group = User.generate!
1102 @second_user_in_group = User.generate!
1085 @user_in_group2 = User.generate!
1103 @user_in_group2 = User.generate!
1086 @user_not_in_group = User.generate!
1104 @user_not_in_group = User.generate!
1087
1105
1088 @group = Group.generate!.reload
1106 @group = Group.generate!.reload
1089 @group.users << @user_in_group
1107 @group.users << @user_in_group
1090 @group.users << @second_user_in_group
1108 @group.users << @second_user_in_group
1091
1109
1092 @group2 = Group.generate!.reload
1110 @group2 = Group.generate!.reload
1093 @group2.users << @user_in_group2
1111 @group2.users << @user_in_group2
1094
1112
1095 end
1113 end
1096
1114
1097 should "search assigned to for users in the group" do
1115 should "search assigned to for users in the group" do
1098 @query = IssueQuery.new(:name => '_')
1116 @query = IssueQuery.new(:name => '_')
1099 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1117 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1100
1118
1101 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')"
1119 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')"
1102 assert_find_issues_with_query_is_successful @query
1120 assert_find_issues_with_query_is_successful @query
1103 end
1121 end
1104
1122
1105 should "search not assigned to any group member (none)" do
1123 should "search not assigned to any group member (none)" do
1106 @query = IssueQuery.new(:name => '_')
1124 @query = IssueQuery.new(:name => '_')
1107 @query.add_filter('member_of_group', '!*', [''])
1125 @query.add_filter('member_of_group', '!*', [''])
1108
1126
1109 # Users not in a group
1127 # Users not in a group
1110 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}')"
1128 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}')"
1111 assert_find_issues_with_query_is_successful @query
1129 assert_find_issues_with_query_is_successful @query
1112 end
1130 end
1113
1131
1114 should "search assigned to any group member (all)" do
1132 should "search assigned to any group member (all)" do
1115 @query = IssueQuery.new(:name => '_')
1133 @query = IssueQuery.new(:name => '_')
1116 @query.add_filter('member_of_group', '*', [''])
1134 @query.add_filter('member_of_group', '*', [''])
1117
1135
1118 # Only users in a group
1136 # Only users in a group
1119 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}')"
1137 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}')"
1120 assert_find_issues_with_query_is_successful @query
1138 assert_find_issues_with_query_is_successful @query
1121 end
1139 end
1122
1140
1123 should "return an empty set with = empty group" do
1141 should "return an empty set with = empty group" do
1124 @empty_group = Group.generate!
1142 @empty_group = Group.generate!
1125 @query = IssueQuery.new(:name => '_')
1143 @query = IssueQuery.new(:name => '_')
1126 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1144 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1127
1145
1128 assert_equal [], find_issues_with_query(@query)
1146 assert_equal [], find_issues_with_query(@query)
1129 end
1147 end
1130
1148
1131 should "return issues with ! empty group" do
1149 should "return issues with ! empty group" do
1132 @empty_group = Group.generate!
1150 @empty_group = Group.generate!
1133 @query = IssueQuery.new(:name => '_')
1151 @query = IssueQuery.new(:name => '_')
1134 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1152 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1135
1153
1136 assert_find_issues_with_query_is_successful @query
1154 assert_find_issues_with_query_is_successful @query
1137 end
1155 end
1138 end
1156 end
1139
1157
1140 context "with 'assigned_to_role' filter" do
1158 context "with 'assigned_to_role' filter" do
1141 setup do
1159 setup do
1142 @manager_role = Role.find_by_name('Manager')
1160 @manager_role = Role.find_by_name('Manager')
1143 @developer_role = Role.find_by_name('Developer')
1161 @developer_role = Role.find_by_name('Developer')
1144
1162
1145 @project = Project.generate!
1163 @project = Project.generate!
1146 @manager = User.generate!
1164 @manager = User.generate!
1147 @developer = User.generate!
1165 @developer = User.generate!
1148 @boss = User.generate!
1166 @boss = User.generate!
1149 @guest = User.generate!
1167 @guest = User.generate!
1150 User.add_to_project(@manager, @project, @manager_role)
1168 User.add_to_project(@manager, @project, @manager_role)
1151 User.add_to_project(@developer, @project, @developer_role)
1169 User.add_to_project(@developer, @project, @developer_role)
1152 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1170 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1153
1171
1154 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1172 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1155 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1173 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1156 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1174 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1157 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1175 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1158 @issue5 = Issue.generate!(:project => @project)
1176 @issue5 = Issue.generate!(:project => @project)
1159 end
1177 end
1160
1178
1161 should "search assigned to for users with the Role" do
1179 should "search assigned to for users with the Role" do
1162 @query = IssueQuery.new(:name => '_', :project => @project)
1180 @query = IssueQuery.new(:name => '_', :project => @project)
1163 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1181 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1164
1182
1165 assert_query_result [@issue1, @issue3], @query
1183 assert_query_result [@issue1, @issue3], @query
1166 end
1184 end
1167
1185
1168 should "search assigned to for users with the Role on the issue project" do
1186 should "search assigned to for users with the Role on the issue project" do
1169 other_project = Project.generate!
1187 other_project = Project.generate!
1170 User.add_to_project(@developer, other_project, @manager_role)
1188 User.add_to_project(@developer, other_project, @manager_role)
1171
1189
1172 @query = IssueQuery.new(:name => '_', :project => @project)
1190 @query = IssueQuery.new(:name => '_', :project => @project)
1173 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1191 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1174
1192
1175 assert_query_result [@issue1, @issue3], @query
1193 assert_query_result [@issue1, @issue3], @query
1176 end
1194 end
1177
1195
1178 should "return an empty set with empty role" do
1196 should "return an empty set with empty role" do
1179 @empty_role = Role.generate!
1197 @empty_role = Role.generate!
1180 @query = IssueQuery.new(:name => '_', :project => @project)
1198 @query = IssueQuery.new(:name => '_', :project => @project)
1181 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1199 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1182
1200
1183 assert_query_result [], @query
1201 assert_query_result [], @query
1184 end
1202 end
1185
1203
1186 should "search assigned to for users without the Role" do
1204 should "search assigned to for users without the Role" do
1187 @query = IssueQuery.new(:name => '_', :project => @project)
1205 @query = IssueQuery.new(:name => '_', :project => @project)
1188 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1206 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1189
1207
1190 assert_query_result [@issue2, @issue4, @issue5], @query
1208 assert_query_result [@issue2, @issue4, @issue5], @query
1191 end
1209 end
1192
1210
1193 should "search assigned to for users not assigned to any Role (none)" do
1211 should "search assigned to for users not assigned to any Role (none)" do
1194 @query = IssueQuery.new(:name => '_', :project => @project)
1212 @query = IssueQuery.new(:name => '_', :project => @project)
1195 @query.add_filter('assigned_to_role', '!*', [''])
1213 @query.add_filter('assigned_to_role', '!*', [''])
1196
1214
1197 assert_query_result [@issue4, @issue5], @query
1215 assert_query_result [@issue4, @issue5], @query
1198 end
1216 end
1199
1217
1200 should "search assigned to for users assigned to any Role (all)" do
1218 should "search assigned to for users assigned to any Role (all)" do
1201 @query = IssueQuery.new(:name => '_', :project => @project)
1219 @query = IssueQuery.new(:name => '_', :project => @project)
1202 @query.add_filter('assigned_to_role', '*', [''])
1220 @query.add_filter('assigned_to_role', '*', [''])
1203
1221
1204 assert_query_result [@issue1, @issue2, @issue3], @query
1222 assert_query_result [@issue1, @issue2, @issue3], @query
1205 end
1223 end
1206
1224
1207 should "return issues with ! empty role" do
1225 should "return issues with ! empty role" do
1208 @empty_role = Role.generate!
1226 @empty_role = Role.generate!
1209 @query = IssueQuery.new(:name => '_', :project => @project)
1227 @query = IssueQuery.new(:name => '_', :project => @project)
1210 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1228 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1211
1229
1212 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1230 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1213 end
1231 end
1214 end
1232 end
1215 end
1233 end
1216 end
1234 end
General Comments 0
You need to be logged in to leave comments. Login now