##// END OF EJS Templates
Adds date based filters (#4729) and date range filter (#6954)....
Jean-Philippe Lang -
r6106:4507aa501418
parent child
Show More
@@ -1,711 +1,736
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !sortable.nil?
39 !sortable.nil?
40 end
40 end
41
41
42 def value(issue)
42 def value(issue)
43 issue.send name
43 issue.send name
44 end
44 end
45
45
46 def css_classes
46 def css_classes
47 name
47 name
48 end
48 end
49 end
49 end
50
50
51 class QueryCustomFieldColumn < QueryColumn
51 class QueryCustomFieldColumn < QueryColumn
52
52
53 def initialize(custom_field)
53 def initialize(custom_field)
54 self.name = "cf_#{custom_field.id}".to_sym
54 self.name = "cf_#{custom_field.id}".to_sym
55 self.sortable = custom_field.order_statement || false
55 self.sortable = custom_field.order_statement || false
56 if %w(list date bool int).include?(custom_field.field_format)
56 if %w(list date bool int).include?(custom_field.field_format)
57 self.groupable = custom_field.order_statement
57 self.groupable = custom_field.order_statement
58 end
58 end
59 self.groupable ||= false
59 self.groupable ||= false
60 @cf = custom_field
60 @cf = custom_field
61 end
61 end
62
62
63 def caption
63 def caption
64 @cf.name
64 @cf.name
65 end
65 end
66
66
67 def custom_field
67 def custom_field
68 @cf
68 @cf
69 end
69 end
70
70
71 def value(issue)
71 def value(issue)
72 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
72 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
73 cv && @cf.cast_value(cv.value)
73 cv && @cf.cast_value(cv.value)
74 end
74 end
75
75
76 def css_classes
76 def css_classes
77 @css_classes ||= "#{name} #{@cf.field_format}"
77 @css_classes ||= "#{name} #{@cf.field_format}"
78 end
78 end
79 end
79 end
80
80
81 class Query < ActiveRecord::Base
81 class Query < ActiveRecord::Base
82 class StatementInvalid < ::ActiveRecord::StatementInvalid
82 class StatementInvalid < ::ActiveRecord::StatementInvalid
83 end
83 end
84
84
85 belongs_to :project
85 belongs_to :project
86 belongs_to :user
86 belongs_to :user
87 serialize :filters
87 serialize :filters
88 serialize :column_names
88 serialize :column_names
89 serialize :sort_criteria, Array
89 serialize :sort_criteria, Array
90
90
91 attr_protected :project_id, :user_id
91 attr_protected :project_id, :user_id
92
92
93 validates_presence_of :name, :on => :save
93 validates_presence_of :name, :on => :save
94 validates_length_of :name, :maximum => 255
94 validates_length_of :name, :maximum => 255
95
95
96 @@operators = { "=" => :label_equals,
96 @@operators = { "=" => :label_equals,
97 "!" => :label_not_equals,
97 "!" => :label_not_equals,
98 "o" => :label_open_issues,
98 "o" => :label_open_issues,
99 "c" => :label_closed_issues,
99 "c" => :label_closed_issues,
100 "!*" => :label_none,
100 "!*" => :label_none,
101 "*" => :label_all,
101 "*" => :label_all,
102 ">=" => :label_greater_or_equal,
102 ">=" => :label_greater_or_equal,
103 "<=" => :label_less_or_equal,
103 "<=" => :label_less_or_equal,
104 "><" => :label_between,
104 "><" => :label_between,
105 "<t+" => :label_in_less_than,
105 "<t+" => :label_in_less_than,
106 ">t+" => :label_in_more_than,
106 ">t+" => :label_in_more_than,
107 "t+" => :label_in,
107 "t+" => :label_in,
108 "t" => :label_today,
108 "t" => :label_today,
109 "w" => :label_this_week,
109 "w" => :label_this_week,
110 ">t-" => :label_less_than_ago,
110 ">t-" => :label_less_than_ago,
111 "<t-" => :label_more_than_ago,
111 "<t-" => :label_more_than_ago,
112 "t-" => :label_ago,
112 "t-" => :label_ago,
113 "~" => :label_contains,
113 "~" => :label_contains,
114 "!~" => :label_not_contains }
114 "!~" => :label_not_contains }
115
115
116 cattr_reader :operators
116 cattr_reader :operators
117
117
118 @@operators_by_filter_type = { :list => [ "=", "!" ],
118 @@operators_by_filter_type = { :list => [ "=", "!" ],
119 :list_status => [ "o", "=", "!", "c", "*" ],
119 :list_status => [ "o", "=", "!", "c", "*" ],
120 :list_optional => [ "=", "!", "!*", "*" ],
120 :list_optional => [ "=", "!", "!*", "*" ],
121 :list_subprojects => [ "*", "!*", "=" ],
121 :list_subprojects => [ "*", "!*", "=" ],
122 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
122 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
123 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
123 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w" ],
124 :string => [ "=", "~", "!", "!~" ],
124 :string => [ "=", "~", "!", "!~" ],
125 :text => [ "~", "!~" ],
125 :text => [ "~", "!~" ],
126 :integer => [ "=", ">=", "<=", "><", "!*", "*" ] }
126 :integer => [ "=", ">=", "<=", "><", "!*", "*" ] }
127
127
128 cattr_reader :operators_by_filter_type
128 cattr_reader :operators_by_filter_type
129
129
130 @@available_columns = [
130 @@available_columns = [
131 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
131 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
132 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
132 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
133 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
133 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
134 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
134 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
135 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
135 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
136 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
136 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
137 QueryColumn.new(:author),
137 QueryColumn.new(:author),
138 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
138 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
139 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
139 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
140 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
140 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
141 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
141 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
142 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
142 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
143 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
143 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
144 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
144 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
145 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
145 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
146 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
146 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
147 ]
147 ]
148 cattr_reader :available_columns
148 cattr_reader :available_columns
149
149
150 named_scope :visible, lambda {|*args|
150 named_scope :visible, lambda {|*args|
151 user = args.shift || User.current
151 user = args.shift || User.current
152 base = Project.allowed_to_condition(user, :view_issues, *args)
152 base = Project.allowed_to_condition(user, :view_issues, *args)
153 user_id = user.logged? ? user.id : 0
153 user_id = user.logged? ? user.id : 0
154 {
154 {
155 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
155 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
156 :include => :project
156 :include => :project
157 }
157 }
158 }
158 }
159
159
160 def initialize(attributes = nil)
160 def initialize(attributes = nil)
161 super attributes
161 super attributes
162 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
162 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
163 end
163 end
164
164
165 def after_initialize
165 def after_initialize
166 # Store the fact that project is nil (used in #editable_by?)
166 # Store the fact that project is nil (used in #editable_by?)
167 @is_for_all = project.nil?
167 @is_for_all = project.nil?
168 end
168 end
169
169
170 def validate
170 def validate
171 filters.each_key do |field|
171 filters.each_key do |field|
172 errors.add label_for(field), :blank unless
172 errors.add label_for(field), :blank unless
173 # filter requires one or more values
173 # filter requires one or more values
174 (values_for(field) and !values_for(field).first.blank?) or
174 (values_for(field) and !values_for(field).first.blank?) or
175 # filter doesn't require any value
175 # filter doesn't require any value
176 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
176 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
177 end if filters
177 end if filters
178 end
178 end
179
179
180 # Returns true if the query is visible to +user+ or the current user.
180 # Returns true if the query is visible to +user+ or the current user.
181 def visible?(user=User.current)
181 def visible?(user=User.current)
182 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
182 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
183 end
183 end
184
184
185 def editable_by?(user)
185 def editable_by?(user)
186 return false unless user
186 return false unless user
187 # Admin can edit them all and regular users can edit their private queries
187 # Admin can edit them all and regular users can edit their private queries
188 return true if user.admin? || (!is_public && self.user_id == user.id)
188 return true if user.admin? || (!is_public && self.user_id == user.id)
189 # Members can not edit public queries that are for all project (only admin is allowed to)
189 # Members can not edit public queries that are for all project (only admin is allowed to)
190 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
190 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
191 end
191 end
192
192
193 def available_filters
193 def available_filters
194 return @available_filters if @available_filters
194 return @available_filters if @available_filters
195
195
196 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
196 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
197
197
198 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
198 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
199 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
199 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
200 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
200 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
201 "subject" => { :type => :text, :order => 8 },
201 "subject" => { :type => :text, :order => 8 },
202 "created_on" => { :type => :date_past, :order => 9 },
202 "created_on" => { :type => :date_past, :order => 9 },
203 "updated_on" => { :type => :date_past, :order => 10 },
203 "updated_on" => { :type => :date_past, :order => 10 },
204 "start_date" => { :type => :date, :order => 11 },
204 "start_date" => { :type => :date, :order => 11 },
205 "due_date" => { :type => :date, :order => 12 },
205 "due_date" => { :type => :date, :order => 12 },
206 "estimated_hours" => { :type => :integer, :order => 13 },
206 "estimated_hours" => { :type => :integer, :order => 13 },
207 "done_ratio" => { :type => :integer, :order => 14 }}
207 "done_ratio" => { :type => :integer, :order => 14 }}
208
208
209 user_values = []
209 user_values = []
210 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
210 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
211 if project
211 if project
212 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
212 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
213 else
213 else
214 all_projects = Project.visible.all
214 all_projects = Project.visible.all
215 if all_projects.any?
215 if all_projects.any?
216 # members of visible projects
216 # members of visible projects
217 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
217 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
218
218
219 # project filter
219 # project filter
220 project_values = []
220 project_values = []
221 Project.project_tree(all_projects) do |p, level|
221 Project.project_tree(all_projects) do |p, level|
222 prefix = (level > 0 ? ('--' * level + ' ') : '')
222 prefix = (level > 0 ? ('--' * level + ' ') : '')
223 project_values << ["#{prefix}#{p.name}", p.id.to_s]
223 project_values << ["#{prefix}#{p.name}", p.id.to_s]
224 end
224 end
225 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
225 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
226 end
226 end
227 end
227 end
228 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
228 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
229 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
229 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
230
230
231 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
231 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
232 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
232 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
233
233
234 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
234 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
235 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
235 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
236
236
237 if User.current.logged?
237 if User.current.logged?
238 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
238 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
239 end
239 end
240
240
241 if project
241 if project
242 # project specific filters
242 # project specific filters
243 categories = @project.issue_categories.all
243 categories = @project.issue_categories.all
244 unless categories.empty?
244 unless categories.empty?
245 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
245 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
246 end
246 end
247 versions = @project.shared_versions.all
247 versions = @project.shared_versions.all
248 unless versions.empty?
248 unless versions.empty?
249 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
249 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
250 end
250 end
251 unless @project.leaf?
251 unless @project.leaf?
252 subprojects = @project.descendants.visible.all
252 subprojects = @project.descendants.visible.all
253 unless subprojects.empty?
253 unless subprojects.empty?
254 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
254 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
255 end
255 end
256 end
256 end
257 add_custom_fields_filters(@project.all_issue_custom_fields)
257 add_custom_fields_filters(@project.all_issue_custom_fields)
258 else
258 else
259 # global filters for cross project issue list
259 # global filters for cross project issue list
260 system_shared_versions = Version.visible.find_all_by_sharing('system')
260 system_shared_versions = Version.visible.find_all_by_sharing('system')
261 unless system_shared_versions.empty?
261 unless system_shared_versions.empty?
262 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
262 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
263 end
263 end
264 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
264 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
265 end
265 end
266 @available_filters
266 @available_filters
267 end
267 end
268
268
269 def add_filter(field, operator, values)
269 def add_filter(field, operator, values)
270 # values must be an array
270 # values must be an array
271 return unless values and values.is_a? Array # and !values.first.empty?
271 return unless values.nil? || values.is_a?(Array)
272 # check if field is defined as an available filter
272 # check if field is defined as an available filter
273 if available_filters.has_key? field
273 if available_filters.has_key? field
274 filter_options = available_filters[field]
274 filter_options = available_filters[field]
275 # check if operator is allowed for that filter
275 # check if operator is allowed for that filter
276 #if @@operators_by_filter_type[filter_options[:type]].include? operator
276 #if @@operators_by_filter_type[filter_options[:type]].include? operator
277 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
277 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
278 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
278 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
279 #end
279 #end
280 filters[field] = {:operator => operator, :values => values }
280 filters[field] = {:operator => operator, :values => (values || ['']) }
281 end
281 end
282 end
282 end
283
283
284 def add_short_filter(field, expression)
284 def add_short_filter(field, expression)
285 return unless expression
285 return unless expression
286 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
286 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
287 add_filter field, (parms[0] || "="), [parms[1] || ""]
287 add_filter field, (parms[0] || "="), [parms[1] || ""]
288 end
288 end
289
289
290 # Add multiple filters using +add_filter+
290 # Add multiple filters using +add_filter+
291 def add_filters(fields, operators, values)
291 def add_filters(fields, operators, values)
292 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
292 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
293 fields.each do |field|
293 fields.each do |field|
294 add_filter(field, operators[field], values[field])
294 add_filter(field, operators[field], values && values[field])
295 end
295 end
296 end
296 end
297 end
297 end
298
298
299 def has_filter?(field)
299 def has_filter?(field)
300 filters and filters[field]
300 filters and filters[field]
301 end
301 end
302
303 def type_for(field)
304 available_filters[field][:type] if available_filters.has_key?(field)
305 end
302
306
303 def operator_for(field)
307 def operator_for(field)
304 has_filter?(field) ? filters[field][:operator] : nil
308 has_filter?(field) ? filters[field][:operator] : nil
305 end
309 end
306
310
307 def values_for(field)
311 def values_for(field)
308 has_filter?(field) ? filters[field][:values] : nil
312 has_filter?(field) ? filters[field][:values] : nil
309 end
313 end
310
314
311 def value_for(field, index=0)
315 def value_for(field, index=0)
312 (values_for(field) || [])[index]
316 (values_for(field) || [])[index]
313 end
317 end
314
318
315 def label_for(field)
319 def label_for(field)
316 label = available_filters[field][:name] if available_filters.has_key?(field)
320 label = available_filters[field][:name] if available_filters.has_key?(field)
317 label ||= field.gsub(/\_id$/, "")
321 label ||= field.gsub(/\_id$/, "")
318 end
322 end
319
323
320 def available_columns
324 def available_columns
321 return @available_columns if @available_columns
325 return @available_columns if @available_columns
322 @available_columns = Query.available_columns
326 @available_columns = Query.available_columns
323 @available_columns += (project ?
327 @available_columns += (project ?
324 project.all_issue_custom_fields :
328 project.all_issue_custom_fields :
325 IssueCustomField.find(:all)
329 IssueCustomField.find(:all)
326 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
330 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
327 end
331 end
328
332
329 def self.available_columns=(v)
333 def self.available_columns=(v)
330 self.available_columns = (v)
334 self.available_columns = (v)
331 end
335 end
332
336
333 def self.add_available_column(column)
337 def self.add_available_column(column)
334 self.available_columns << (column) if column.is_a?(QueryColumn)
338 self.available_columns << (column) if column.is_a?(QueryColumn)
335 end
339 end
336
340
337 # Returns an array of columns that can be used to group the results
341 # Returns an array of columns that can be used to group the results
338 def groupable_columns
342 def groupable_columns
339 available_columns.select {|c| c.groupable}
343 available_columns.select {|c| c.groupable}
340 end
344 end
341
345
342 # Returns a Hash of columns and the key for sorting
346 # Returns a Hash of columns and the key for sorting
343 def sortable_columns
347 def sortable_columns
344 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
348 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
345 h[column.name.to_s] = column.sortable
349 h[column.name.to_s] = column.sortable
346 h
350 h
347 })
351 })
348 end
352 end
349
353
350 def columns
354 def columns
351 if has_default_columns?
355 if has_default_columns?
352 available_columns.select do |c|
356 available_columns.select do |c|
353 # Adds the project column by default for cross-project lists
357 # Adds the project column by default for cross-project lists
354 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
358 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
355 end
359 end
356 else
360 else
357 # preserve the column_names order
361 # preserve the column_names order
358 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
362 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
359 end
363 end
360 end
364 end
361
365
362 def column_names=(names)
366 def column_names=(names)
363 if names
367 if names
364 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
368 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
365 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
369 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
366 # Set column_names to nil if default columns
370 # Set column_names to nil if default columns
367 if names.map(&:to_s) == Setting.issue_list_default_columns
371 if names.map(&:to_s) == Setting.issue_list_default_columns
368 names = nil
372 names = nil
369 end
373 end
370 end
374 end
371 write_attribute(:column_names, names)
375 write_attribute(:column_names, names)
372 end
376 end
373
377
374 def has_column?(column)
378 def has_column?(column)
375 column_names && column_names.include?(column.name)
379 column_names && column_names.include?(column.name)
376 end
380 end
377
381
378 def has_default_columns?
382 def has_default_columns?
379 column_names.nil? || column_names.empty?
383 column_names.nil? || column_names.empty?
380 end
384 end
381
385
382 def sort_criteria=(arg)
386 def sort_criteria=(arg)
383 c = []
387 c = []
384 if arg.is_a?(Hash)
388 if arg.is_a?(Hash)
385 arg = arg.keys.sort.collect {|k| arg[k]}
389 arg = arg.keys.sort.collect {|k| arg[k]}
386 end
390 end
387 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
391 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
388 write_attribute(:sort_criteria, c)
392 write_attribute(:sort_criteria, c)
389 end
393 end
390
394
391 def sort_criteria
395 def sort_criteria
392 read_attribute(:sort_criteria) || []
396 read_attribute(:sort_criteria) || []
393 end
397 end
394
398
395 def sort_criteria_key(arg)
399 def sort_criteria_key(arg)
396 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
400 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
397 end
401 end
398
402
399 def sort_criteria_order(arg)
403 def sort_criteria_order(arg)
400 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
404 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
401 end
405 end
402
406
403 # Returns the SQL sort order that should be prepended for grouping
407 # Returns the SQL sort order that should be prepended for grouping
404 def group_by_sort_order
408 def group_by_sort_order
405 if grouped? && (column = group_by_column)
409 if grouped? && (column = group_by_column)
406 column.sortable.is_a?(Array) ?
410 column.sortable.is_a?(Array) ?
407 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
411 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
408 "#{column.sortable} #{column.default_order}"
412 "#{column.sortable} #{column.default_order}"
409 end
413 end
410 end
414 end
411
415
412 # Returns true if the query is a grouped query
416 # Returns true if the query is a grouped query
413 def grouped?
417 def grouped?
414 !group_by_column.nil?
418 !group_by_column.nil?
415 end
419 end
416
420
417 def group_by_column
421 def group_by_column
418 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
422 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
419 end
423 end
420
424
421 def group_by_statement
425 def group_by_statement
422 group_by_column.try(:groupable)
426 group_by_column.try(:groupable)
423 end
427 end
424
428
425 def project_statement
429 def project_statement
426 project_clauses = []
430 project_clauses = []
427 if project && !@project.descendants.active.empty?
431 if project && !@project.descendants.active.empty?
428 ids = [project.id]
432 ids = [project.id]
429 if has_filter?("subproject_id")
433 if has_filter?("subproject_id")
430 case operator_for("subproject_id")
434 case operator_for("subproject_id")
431 when '='
435 when '='
432 # include the selected subprojects
436 # include the selected subprojects
433 ids += values_for("subproject_id").each(&:to_i)
437 ids += values_for("subproject_id").each(&:to_i)
434 when '!*'
438 when '!*'
435 # main project only
439 # main project only
436 else
440 else
437 # all subprojects
441 # all subprojects
438 ids += project.descendants.collect(&:id)
442 ids += project.descendants.collect(&:id)
439 end
443 end
440 elsif Setting.display_subprojects_issues?
444 elsif Setting.display_subprojects_issues?
441 ids += project.descendants.collect(&:id)
445 ids += project.descendants.collect(&:id)
442 end
446 end
443 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
447 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
444 elsif project
448 elsif project
445 project_clauses << "#{Project.table_name}.id = %d" % project.id
449 project_clauses << "#{Project.table_name}.id = %d" % project.id
446 end
450 end
447 project_clauses.any? ? project_clauses.join(' AND ') : nil
451 project_clauses.any? ? project_clauses.join(' AND ') : nil
448 end
452 end
449
453
450 def statement
454 def statement
451 # filters clauses
455 # filters clauses
452 filters_clauses = []
456 filters_clauses = []
453 filters.each_key do |field|
457 filters.each_key do |field|
454 next if field == "subproject_id"
458 next if field == "subproject_id"
455 v = values_for(field).clone
459 v = values_for(field).clone
456 next unless v and !v.empty?
460 next unless v and !v.empty?
457 operator = operator_for(field)
461 operator = operator_for(field)
458
462
459 # "me" value subsitution
463 # "me" value subsitution
460 if %w(assigned_to_id author_id watcher_id).include?(field)
464 if %w(assigned_to_id author_id watcher_id).include?(field)
461 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
465 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
462 end
466 end
463
467
464 sql = ''
468 sql = ''
465 if field =~ /^cf_(\d+)$/
469 if field =~ /^cf_(\d+)$/
466 # custom field
470 # custom field
467 db_table = CustomValue.table_name
471 db_table = CustomValue.table_name
468 db_field = 'value'
472 db_field = 'value'
469 is_custom_filter = true
473 is_custom_filter = true
470 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
474 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
471 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
475 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
472 elsif field == 'watcher_id'
476 elsif field == 'watcher_id'
473 db_table = Watcher.table_name
477 db_table = Watcher.table_name
474 db_field = 'user_id'
478 db_field = 'user_id'
475 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
479 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
476 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
480 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
477 elsif field == "member_of_group" # named field
481 elsif field == "member_of_group" # named field
478 if operator == '*' # Any group
482 if operator == '*' # Any group
479 groups = Group.all
483 groups = Group.all
480 operator = '=' # Override the operator since we want to find by assigned_to
484 operator = '=' # Override the operator since we want to find by assigned_to
481 elsif operator == "!*"
485 elsif operator == "!*"
482 groups = Group.all
486 groups = Group.all
483 operator = '!' # Override the operator since we want to find by assigned_to
487 operator = '!' # Override the operator since we want to find by assigned_to
484 else
488 else
485 groups = Group.find_all_by_id(v)
489 groups = Group.find_all_by_id(v)
486 end
490 end
487 groups ||= []
491 groups ||= []
488
492
489 members_of_groups = groups.inject([]) {|user_ids, group|
493 members_of_groups = groups.inject([]) {|user_ids, group|
490 if group && group.user_ids.present?
494 if group && group.user_ids.present?
491 user_ids << group.user_ids
495 user_ids << group.user_ids
492 end
496 end
493 user_ids.flatten.uniq.compact
497 user_ids.flatten.uniq.compact
494 }.sort.collect(&:to_s)
498 }.sort.collect(&:to_s)
495
499
496 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
500 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
497
501
498 elsif field == "assigned_to_role" # named field
502 elsif field == "assigned_to_role" # named field
499 if operator == "*" # Any Role
503 if operator == "*" # Any Role
500 roles = Role.givable
504 roles = Role.givable
501 operator = '=' # Override the operator since we want to find by assigned_to
505 operator = '=' # Override the operator since we want to find by assigned_to
502 elsif operator == "!*" # No role
506 elsif operator == "!*" # No role
503 roles = Role.givable
507 roles = Role.givable
504 operator = '!' # Override the operator since we want to find by assigned_to
508 operator = '!' # Override the operator since we want to find by assigned_to
505 else
509 else
506 roles = Role.givable.find_all_by_id(v)
510 roles = Role.givable.find_all_by_id(v)
507 end
511 end
508 roles ||= []
512 roles ||= []
509
513
510 members_of_roles = roles.inject([]) {|user_ids, role|
514 members_of_roles = roles.inject([]) {|user_ids, role|
511 if role && role.members
515 if role && role.members
512 user_ids << role.members.collect(&:user_id)
516 user_ids << role.members.collect(&:user_id)
513 end
517 end
514 user_ids.flatten.uniq.compact
518 user_ids.flatten.uniq.compact
515 }.sort.collect(&:to_s)
519 }.sort.collect(&:to_s)
516
520
517 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
521 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
518 else
522 else
519 # regular field
523 # regular field
520 db_table = Issue.table_name
524 db_table = Issue.table_name
521 db_field = field
525 db_field = field
522 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
526 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
523 end
527 end
524 filters_clauses << sql
528 filters_clauses << sql
525
529
526 end if filters and valid?
530 end if filters and valid?
527
531
528 filters_clauses << project_statement
532 filters_clauses << project_statement
529 filters_clauses.reject!(&:blank?)
533 filters_clauses.reject!(&:blank?)
530
534
531 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
535 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
532 end
536 end
533
537
534 # Returns the issue count
538 # Returns the issue count
535 def issue_count
539 def issue_count
536 Issue.count(:include => [:status, :project], :conditions => statement)
540 Issue.count(:include => [:status, :project], :conditions => statement)
537 rescue ::ActiveRecord::StatementInvalid => e
541 rescue ::ActiveRecord::StatementInvalid => e
538 raise StatementInvalid.new(e.message)
542 raise StatementInvalid.new(e.message)
539 end
543 end
540
544
541 # Returns the issue count by group or nil if query is not grouped
545 # Returns the issue count by group or nil if query is not grouped
542 def issue_count_by_group
546 def issue_count_by_group
543 r = nil
547 r = nil
544 if grouped?
548 if grouped?
545 begin
549 begin
546 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
550 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
547 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
551 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
548 rescue ActiveRecord::RecordNotFound
552 rescue ActiveRecord::RecordNotFound
549 r = {nil => issue_count}
553 r = {nil => issue_count}
550 end
554 end
551 c = group_by_column
555 c = group_by_column
552 if c.is_a?(QueryCustomFieldColumn)
556 if c.is_a?(QueryCustomFieldColumn)
553 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
557 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
554 end
558 end
555 end
559 end
556 r
560 r
557 rescue ::ActiveRecord::StatementInvalid => e
561 rescue ::ActiveRecord::StatementInvalid => e
558 raise StatementInvalid.new(e.message)
562 raise StatementInvalid.new(e.message)
559 end
563 end
560
564
561 # Returns the issues
565 # Returns the issues
562 # Valid options are :order, :offset, :limit, :include, :conditions
566 # Valid options are :order, :offset, :limit, :include, :conditions
563 def issues(options={})
567 def issues(options={})
564 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
568 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
565 order_option = nil if order_option.blank?
569 order_option = nil if order_option.blank?
566
570
567 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
571 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
568 :conditions => Query.merge_conditions(statement, options[:conditions]),
572 :conditions => Query.merge_conditions(statement, options[:conditions]),
569 :order => order_option,
573 :order => order_option,
570 :limit => options[:limit],
574 :limit => options[:limit],
571 :offset => options[:offset]
575 :offset => options[:offset]
572 rescue ::ActiveRecord::StatementInvalid => e
576 rescue ::ActiveRecord::StatementInvalid => e
573 raise StatementInvalid.new(e.message)
577 raise StatementInvalid.new(e.message)
574 end
578 end
575
579
576 # Returns the journals
580 # Returns the journals
577 # Valid options are :order, :offset, :limit
581 # Valid options are :order, :offset, :limit
578 def journals(options={})
582 def journals(options={})
579 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
583 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
580 :conditions => statement,
584 :conditions => statement,
581 :order => options[:order],
585 :order => options[:order],
582 :limit => options[:limit],
586 :limit => options[:limit],
583 :offset => options[:offset]
587 :offset => options[:offset]
584 rescue ::ActiveRecord::StatementInvalid => e
588 rescue ::ActiveRecord::StatementInvalid => e
585 raise StatementInvalid.new(e.message)
589 raise StatementInvalid.new(e.message)
586 end
590 end
587
591
588 # Returns the versions
592 # Returns the versions
589 # Valid options are :conditions
593 # Valid options are :conditions
590 def versions(options={})
594 def versions(options={})
591 Version.visible.find :all, :include => :project,
595 Version.visible.find :all, :include => :project,
592 :conditions => Query.merge_conditions(project_statement, options[:conditions])
596 :conditions => Query.merge_conditions(project_statement, options[:conditions])
593 rescue ::ActiveRecord::StatementInvalid => e
597 rescue ::ActiveRecord::StatementInvalid => e
594 raise StatementInvalid.new(e.message)
598 raise StatementInvalid.new(e.message)
595 end
599 end
596
600
597 private
601 private
598
602
599 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
603 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
600 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
604 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
601 sql = ''
605 sql = ''
602 case operator
606 case operator
603 when "="
607 when "="
604 if value.any?
608 if [:date, :date_past].include?(type_for(field))
605 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
609 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
606 else
610 else
607 # IN an empty set
611 if value.any?
608 sql = "1=0"
612 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
613 else
614 # IN an empty set
615 sql = "1=0"
616 end
609 end
617 end
610 when "!"
618 when "!"
611 if value.any?
619 if value.any?
612 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
620 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
613 else
621 else
614 # NOT IN an empty set
622 # NOT IN an empty set
615 sql = "1=1"
623 sql = "1=1"
616 end
624 end
617 when "!*"
625 when "!*"
618 sql = "#{db_table}.#{db_field} IS NULL"
626 sql = "#{db_table}.#{db_field} IS NULL"
619 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
627 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
620 when "*"
628 when "*"
621 sql = "#{db_table}.#{db_field} IS NOT NULL"
629 sql = "#{db_table}.#{db_field} IS NOT NULL"
622 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
630 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
623 when ">="
631 when ">="
624 if is_custom_filter
632 if [:date, :date_past].include?(type_for(field))
625 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_i}"
633 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
626 else
634 else
627 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
635 if is_custom_filter
636 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_i}"
637 else
638 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
639 end
628 end
640 end
629 when "<="
641 when "<="
630 if is_custom_filter
642 if [:date, :date_past].include?(type_for(field))
631 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_i}"
643 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
632 else
644 else
633 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
645 if is_custom_filter
646 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_i}"
647 else
648 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
649 end
634 end
650 end
635 when "><"
651 when "><"
636 if is_custom_filter
652 if [:date, :date_past].include?(type_for(field))
637 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_i} AND #{value[1].to_i}"
653 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
638 else
654 else
639 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_i} AND #{value[1].to_i}"
655 if is_custom_filter
656 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_i} AND #{value[1].to_i}"
657 else
658 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_i} AND #{value[1].to_i}"
659 end
640 end
660 end
641 when "o"
661 when "o"
642 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
662 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
643 when "c"
663 when "c"
644 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
664 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
645 when ">t-"
665 when ">t-"
646 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
666 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
647 when "<t-"
667 when "<t-"
648 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
668 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
649 when "t-"
669 when "t-"
650 sql = date_range_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)
651 when ">t+"
671 when ">t+"
652 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
672 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
653 when "<t+"
673 when "<t+"
654 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
674 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
655 when "t+"
675 when "t+"
656 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
676 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
657 when "t"
677 when "t"
658 sql = date_range_clause(db_table, db_field, 0, 0)
678 sql = relative_date_clause(db_table, db_field, 0, 0)
659 when "w"
679 when "w"
660 first_day_of_week = l(:general_first_day_of_week).to_i
680 first_day_of_week = l(:general_first_day_of_week).to_i
661 day_of_week = Date.today.cwday
681 day_of_week = Date.today.cwday
662 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
682 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
663 sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6)
683 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
664 when "~"
684 when "~"
665 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
685 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
666 when "!~"
686 when "!~"
667 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
687 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
668 else
688 else
669 raise "Unknown query operator #{operator}"
689 raise "Unknown query operator #{operator}"
670 end
690 end
671
691
672 return sql
692 return sql
673 end
693 end
674
694
675 def add_custom_fields_filters(custom_fields)
695 def add_custom_fields_filters(custom_fields)
676 @available_filters ||= {}
696 @available_filters ||= {}
677
697
678 custom_fields.select(&:is_filter?).each do |field|
698 custom_fields.select(&:is_filter?).each do |field|
679 case field.field_format
699 case field.field_format
680 when "text"
700 when "text"
681 options = { :type => :text, :order => 20 }
701 options = { :type => :text, :order => 20 }
682 when "list"
702 when "list"
683 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
703 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
684 when "date"
704 when "date"
685 options = { :type => :date, :order => 20 }
705 options = { :type => :date, :order => 20 }
686 when "bool"
706 when "bool"
687 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
707 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
688 when "int", "float"
708 when "int", "float"
689 options = { :type => :integer, :order => 20 }
709 options = { :type => :integer, :order => 20 }
690 when "user", "version"
710 when "user", "version"
691 next unless project
711 next unless project
692 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
712 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
693 else
713 else
694 options = { :type => :string, :order => 20 }
714 options = { :type => :string, :order => 20 }
695 end
715 end
696 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
716 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
697 end
717 end
698 end
718 end
699
719
700 # Returns a SQL clause for a date or datetime field.
720 # Returns a SQL clause for a date or datetime field.
701 def date_range_clause(table, field, from, to)
721 def date_clause(table, field, from, to)
702 s = []
722 s = []
703 if from
723 if from
704 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
724 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((from - 1).to_time.end_of_day)])
705 end
725 end
706 if to
726 if to
707 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
727 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to.to_time.end_of_day)])
708 end
728 end
709 s.join(' AND ')
729 s.join(' AND ')
710 end
730 end
731
732 # Returns a SQL clause for a date or datetime field using relative dates.
733 def relative_date_clause(table, field, days_from, days_to)
734 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
735 end
711 end
736 end
@@ -1,130 +1,152
1 <script type="text/javascript">
1 <script type="text/javascript">
2 //<![CDATA[
2 //<![CDATA[
3 function add_filter() {
3 function add_filter() {
4 select = $('add_filter_select');
4 select = $('add_filter_select');
5 field = select.value
5 field = select.value
6 Element.show('tr_' + field);
6 Element.show('tr_' + field);
7 check_box = $('cb_' + field);
7 check_box = $('cb_' + field);
8 check_box.checked = true;
8 check_box.checked = true;
9 toggle_filter(field);
9 toggle_filter(field);
10 select.selectedIndex = 0;
10 select.selectedIndex = 0;
11
11
12 for (i=0; i<select.options.length; i++) {
12 for (i=0; i<select.options.length; i++) {
13 if (select.options[i].value == field) {
13 if (select.options[i].value == field) {
14 select.options[i].disabled = true;
14 select.options[i].disabled = true;
15 }
15 }
16 }
16 }
17 }
17 }
18
18
19 function toggle_filter(field) {
19 function toggle_filter(field) {
20 check_box = $('cb_' + field);
20 check_box = $('cb_' + field);
21
21 if (check_box.checked) {
22 if (check_box.checked) {
22 Element.show("operators_" + field);
23 Element.show("operators_" + field);
23 Form.Element.enable("operators_" + field);
24 Form.Element.enable("operators_" + field);
24 toggle_operator(field);
25 $$(".values_" + field).each(function(el){ Form.Element.enable(el)});
25 } else {
26 toggle_operator(field);
26 Element.hide("operators_" + field);
27 } else {
27 Form.Element.disable("operators_" + field);
28 Element.hide("operators_" + field);
28 enableValues(field, []);
29 Element.hide("div_values_" + field);
30 Form.Element.disable("operators_" + field);
31 $$(".values_" + field).each(function(el){ Form.Element.disable(el)});
32 }
29 }
33 }
30 }
34
31
32 function enableValues(field, indexes) {
33 var f = $$(".values_" + field);
34 for(var i=0;i<f.length;i++) {
35 if (indexes.include(i)) {
36 Form.Element.enable(f[i]);
37 f[i].up('span').show();
38 } else {
39 f[i].value = '';
40 Form.Element.disable(f[i]);
41 f[i].up('span').hide();
42 }
43 }
44 if (indexes.length > 0) {
45 Element.show("div_values_" + field);
46 } else {
47 Element.hide("div_values_" + field);
48 }
49 }
50
35 function toggle_operator(field) {
51 function toggle_operator(field) {
36 operator = $("operators_" + field);
52 operator = $("operators_" + field);
37 switch (operator.value) {
53 switch (operator.value) {
38 case "!*":
54 case "!*":
39 case "*":
55 case "*":
40 case "t":
56 case "t":
41 case "w":
57 case "w":
42 case "o":
58 case "o":
43 case "c":
59 case "c":
44 Element.hide("div_values_" + field);
60 enableValues(field, []);
45 var v = $$(".values_" + field);
46 if (v.length > 1) {v[1].hide(); Form.Element.disable(v[1])}
47 break;
61 break;
48 case "><":
62 case "><":
49 Element.show("div_values_" + field);
63 enableValues(field, [0,1]);
50 var v = $$(".values_" + field);
64 break;
51 if (v.length > 1) {v[1].show(); Form.Element.enable(v[1])}
65 case "<t+":
66 case ">t+":
67 case "t+":
68 case ">t-":
69 case "<t-":
70 case "t-":
71 enableValues(field, [2]);
52 break;
72 break;
53 default:
73 default:
54 Element.show("div_values_" + field);
74 enableValues(field, [0]);
55 var v = $$(".values_" + field);
56 if (v.length > 1) {v[1].hide(); Form.Element.disable(v[1])}
57 break;
75 break;
58 }
76 }
59 }
77 }
60
78
61 function toggle_multi_select(field) {
79 function toggle_multi_select(el) {
62 select = $('values_' + field);
80 var select = $(el);
63 if (select.multiple == true) {
81 if (select.multiple == true) {
64 select.multiple = false;
82 select.multiple = false;
65 } else {
83 } else {
66 select.multiple = true;
84 select.multiple = true;
67 }
85 }
68 }
86 }
69
87
70 function submit_query_form(id) {
88 function submit_query_form(id) {
71 selectAllOptions("selected_columns");
89 selectAllOptions("selected_columns");
72 $(id).submit();
90 $(id).submit();
73 }
91 }
74
92
75 function apply_filters_observer() {
93 function apply_filters_observer() {
76 $$("#query_form input[type=text]").invoke("observe", "keypress", function(e){
94 $$("#query_form input[type=text]").invoke("observe", "keypress", function(e){
77 if(e.keyCode == Event.KEY_RETURN) {
95 if(e.keyCode == Event.KEY_RETURN) {
78 submit_query_form("query_form");
96 submit_query_form("query_form");
79 }
97 }
80 });
98 });
81 }
99 }
82 Event.observe(document,"dom:loaded", apply_filters_observer);
100 Event.observe(document,"dom:loaded", apply_filters_observer);
83 //]]>
101 //]]>
84 </script>
102 </script>
85
103
86 <table width="100%">
104 <table width="100%">
87 <tr>
105 <tr>
88 <td>
106 <td>
89 <table>
107 <table>
90 <% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %>
108 <% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %>
91 <% field = filter[0]
109 <% field = filter[0]
92 options = filter[1] %>
110 options = filter[1] %>
93 <tr <%= 'style="display:none;"' unless query.has_filter?(field) %> id="tr_<%= field %>" class="filter">
111 <tr <%= 'style="display:none;"' unless query.has_filter?(field) %> id="tr_<%= field %>" class="filter">
94 <td style="width:200px;">
112 <td style="width:200px;">
95 <%= check_box_tag 'f[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
113 <%= check_box_tag 'f[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
96 <label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
114 <label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
97 </td>
115 </td>
98 <td style="width:170px;">
116 <td style="width:170px;">
99 <%= select_tag "op[#{field}]", options_for_select(operators_for_select(options[:type]), query.operator_for(field)), :id => "operators_#{field}", :onchange => "toggle_operator('#{field}');", :style => "vertical-align: top;" %>
117 <%= select_tag "op[#{field}]", options_for_select(operators_for_select(options[:type]), query.operator_for(field)), :id => "operators_#{field}", :onchange => "toggle_operator('#{field}');", :style => "vertical-align: top;" %>
100 </td>
118 </td>
101 <td>
119 <td>
102 <div id="div_values_<%= field %>" style="display:none;">
120 <div id="div_values_<%= field %>" style="display:none;">
103 <% case options[:type]
121 <% case options[:type]
104 when :list, :list_optional, :list_status, :list_subprojects %>
122 when :list, :list_optional, :list_status, :list_subprojects %>
105 <%= select_tag "v[#{field}][]", options_for_select(options[:values], query.values_for(field)), :class => "values_#{field}", :multiple => (query.values_for(field) && query.values_for(field).length > 1) %>
123 <span class="span_values_<%= field %>">
106 <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('#{field}');", :style => "vertical-align: bottom;" %>
124 <%= select_tag "v[#{field}][]", options_for_select(options[:values], query.values_for(field)), :class => "values_#{field}", :id => "values_#{field}_1", :multiple => (query.values_for(field) && query.values_for(field).length > 1) %>
125 <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('values_#{field}_1');", :style => "vertical-align: bottom;" %>
126 </span>
107 <% when :date, :date_past %>
127 <% when :date, :date_past %>
108 <%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :size => 3 %> <%= l(:label_day_plural) %>
128 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :size => 10, :class => "values_#{field}", :id => "values_#{field}_1" %> <%= calendar_for "values_#{field}_1" %></span>
129 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :size => 10, :class => "values_#{field}", :id => "values_#{field}_2" %> <%= calendar_for "values_#{field}_2" %></span>
130 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :size => 3, :class => "values_#{field}" %> <%= l(:label_day_plural) %></span>
109 <% when :string, :text %>
131 <% when :string, :text %>
110 <%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :size => 30 %>
132 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :id => "values_#{field}", :size => 30 %></span>
111 <% when :integer %>
133 <% when :integer %>
112 <%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :size => 3 %>
134 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :id => "values_#{field}_1", :size => 3 %></span>
113 <%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :class => "values_#{field}", :size => 3 %>
135 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :class => "values_#{field}", :id => "values_#{field}_2", :size => 3 %></span>
114 <% end %>
136 <% end %>
115 </div>
137 </div>
116 <script type="text/javascript">toggle_filter('<%= field %>');</script>
138 <script type="text/javascript">toggle_filter('<%= field %>');</script>
117 </td>
139 </td>
118 </tr>
140 </tr>
119 <% end %>
141 <% end %>
120 </table>
142 </table>
121 </td>
143 </td>
122 <td class="add-filter">
144 <td class="add-filter">
123 <%= label_tag('add_filter_select', l(:label_filter_add)) %>
145 <%= label_tag('add_filter_select', l(:label_filter_add)) %>
124 <%= select_tag 'add_filter_select', options_for_select([["",""]] + query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.collect{|field| [ field[1][:name] || l(("field_"+field[0].to_s.gsub(/_id$/, "")).to_sym), field[0]] unless query.has_filter?(field[0])}.compact),
146 <%= select_tag 'add_filter_select', options_for_select([["",""]] + query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.collect{|field| [ field[1][:name] || l(("field_"+field[0].to_s.gsub(/_id$/, "")).to_sym), field[0]] unless query.has_filter?(field[0])}.compact),
125 :onchange => "add_filter();",
147 :onchange => "add_filter();",
126 :name => nil %>
148 :name => nil %>
127 </td>
149 </td>
128 </tr>
150 </tr>
129 </table>
151 </table>
130 <%= hidden_field_tag 'f[]', '' %>
152 <%= hidden_field_tag 'f[]', '' %>
@@ -1,655 +1,683
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
22
22
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 query = Query.new(:project => nil, :name => '_')
24 query = Query.new(:project => nil, :name => '_')
25 assert query.available_filters.has_key?('cf_1')
25 assert query.available_filters.has_key?('cf_1')
26 assert !query.available_filters.has_key?('cf_3')
26 assert !query.available_filters.has_key?('cf_3')
27 end
27 end
28
28
29 def test_system_shared_versions_should_be_available_in_global_queries
29 def test_system_shared_versions_should_be_available_in_global_queries
30 Version.find(2).update_attribute :sharing, 'system'
30 Version.find(2).update_attribute :sharing, 'system'
31 query = Query.new(:project => nil, :name => '_')
31 query = Query.new(:project => nil, :name => '_')
32 assert query.available_filters.has_key?('fixed_version_id')
32 assert query.available_filters.has_key?('fixed_version_id')
33 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
33 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
34 end
34 end
35
35
36 def test_project_filter_in_global_queries
36 def test_project_filter_in_global_queries
37 query = Query.new(:project => nil, :name => '_')
37 query = Query.new(:project => nil, :name => '_')
38 project_filter = query.available_filters["project_id"]
38 project_filter = query.available_filters["project_id"]
39 assert_not_nil project_filter
39 assert_not_nil project_filter
40 project_ids = project_filter[:values].map{|p| p[1]}
40 project_ids = project_filter[:values].map{|p| p[1]}
41 assert project_ids.include?("1") #public project
41 assert project_ids.include?("1") #public project
42 assert !project_ids.include?("2") #private project user cannot see
42 assert !project_ids.include?("2") #private project user cannot see
43 end
43 end
44
44
45 def find_issues_with_query(query)
45 def find_issues_with_query(query)
46 Issue.find :all,
46 Issue.find :all,
47 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
47 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
48 :conditions => query.statement
48 :conditions => query.statement
49 end
49 end
50
50
51 def assert_find_issues_with_query_is_successful(query)
51 def assert_find_issues_with_query_is_successful(query)
52 assert_nothing_raised do
52 assert_nothing_raised do
53 find_issues_with_query(query)
53 find_issues_with_query(query)
54 end
54 end
55 end
55 end
56
56
57 def assert_query_statement_includes(query, condition)
57 def assert_query_statement_includes(query, condition)
58 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
58 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
59 end
59 end
60
60
61 def test_query_should_allow_shared_versions_for_a_project_query
61 def test_query_should_allow_shared_versions_for_a_project_query
62 subproject_version = Version.find(4)
62 subproject_version = Version.find(4)
63 query = Query.new(:project => Project.find(1), :name => '_')
63 query = Query.new(:project => Project.find(1), :name => '_')
64 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
64 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
65
65
66 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
66 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
67 end
67 end
68
68
69 def test_query_with_multiple_custom_fields
69 def test_query_with_multiple_custom_fields
70 query = Query.find(1)
70 query = Query.find(1)
71 assert query.valid?
71 assert query.valid?
72 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
72 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
73 issues = find_issues_with_query(query)
73 issues = find_issues_with_query(query)
74 assert_equal 1, issues.length
74 assert_equal 1, issues.length
75 assert_equal Issue.find(3), issues.first
75 assert_equal Issue.find(3), issues.first
76 end
76 end
77
77
78 def test_operator_none
78 def test_operator_none
79 query = Query.new(:project => Project.find(1), :name => '_')
79 query = Query.new(:project => Project.find(1), :name => '_')
80 query.add_filter('fixed_version_id', '!*', [''])
80 query.add_filter('fixed_version_id', '!*', [''])
81 query.add_filter('cf_1', '!*', [''])
81 query.add_filter('cf_1', '!*', [''])
82 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
82 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
83 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
83 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
84 find_issues_with_query(query)
84 find_issues_with_query(query)
85 end
85 end
86
86
87 def test_operator_none_for_integer
87 def test_operator_none_for_integer
88 query = Query.new(:project => Project.find(1), :name => '_')
88 query = Query.new(:project => Project.find(1), :name => '_')
89 query.add_filter('estimated_hours', '!*', [''])
89 query.add_filter('estimated_hours', '!*', [''])
90 issues = find_issues_with_query(query)
90 issues = find_issues_with_query(query)
91 assert !issues.empty?
91 assert !issues.empty?
92 assert issues.all? {|i| !i.estimated_hours}
92 assert issues.all? {|i| !i.estimated_hours}
93 end
93 end
94
94
95 def test_operator_all
95 def test_operator_all
96 query = Query.new(:project => Project.find(1), :name => '_')
96 query = Query.new(:project => Project.find(1), :name => '_')
97 query.add_filter('fixed_version_id', '*', [''])
97 query.add_filter('fixed_version_id', '*', [''])
98 query.add_filter('cf_1', '*', [''])
98 query.add_filter('cf_1', '*', [''])
99 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
99 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
100 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
100 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
101 find_issues_with_query(query)
101 find_issues_with_query(query)
102 end
102 end
103
103
104 def test_operator_greater_than
104 def test_operator_greater_than
105 query = Query.new(:project => Project.find(1), :name => '_')
105 query = Query.new(:project => Project.find(1), :name => '_')
106 query.add_filter('done_ratio', '>=', ['40'])
106 query.add_filter('done_ratio', '>=', ['40'])
107 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
107 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
108 find_issues_with_query(query)
108 find_issues_with_query(query)
109 end
109 end
110
110
111 def test_operator_greater_than_on_custom_field
111 def test_operator_greater_than_on_custom_field
112 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
112 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
113 query = Query.new(:project => Project.find(1), :name => '_')
113 query = Query.new(:project => Project.find(1), :name => '_')
114 query.add_filter("cf_#{f.id}", '>=', ['40'])
114 query.add_filter("cf_#{f.id}", '>=', ['40'])
115 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) >= 40")
115 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) >= 40")
116 find_issues_with_query(query)
116 find_issues_with_query(query)
117 end
117 end
118
118
119 def test_operator_lesser_than
119 def test_operator_lesser_than
120 query = Query.new(:project => Project.find(1), :name => '_')
120 query = Query.new(:project => Project.find(1), :name => '_')
121 query.add_filter('done_ratio', '<=', ['30'])
121 query.add_filter('done_ratio', '<=', ['30'])
122 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30")
122 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30")
123 find_issues_with_query(query)
123 find_issues_with_query(query)
124 end
124 end
125
125
126 def test_operator_lesser_than_on_custom_field
126 def test_operator_lesser_than_on_custom_field
127 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
127 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
128 query = Query.new(:project => Project.find(1), :name => '_')
128 query = Query.new(:project => Project.find(1), :name => '_')
129 query.add_filter("cf_#{f.id}", '<=', ['30'])
129 query.add_filter("cf_#{f.id}", '<=', ['30'])
130 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30")
130 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30")
131 find_issues_with_query(query)
131 find_issues_with_query(query)
132 end
132 end
133
133
134 def test_operator_between
134 def test_operator_between
135 query = Query.new(:project => Project.find(1), :name => '_')
135 query = Query.new(:project => Project.find(1), :name => '_')
136 query.add_filter('done_ratio', '><', ['30', '40'])
136 query.add_filter('done_ratio', '><', ['30', '40'])
137 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30 AND 40", query.statement
137 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30 AND 40", query.statement
138 find_issues_with_query(query)
138 find_issues_with_query(query)
139 end
139 end
140
140
141 def test_operator_between_on_custom_field
141 def test_operator_between_on_custom_field
142 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
142 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
143 query = Query.new(:project => Project.find(1), :name => '_')
143 query = Query.new(:project => Project.find(1), :name => '_')
144 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
144 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
145 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30 AND 40", query.statement
145 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30 AND 40", query.statement
146 find_issues_with_query(query)
146 find_issues_with_query(query)
147 end
147 end
148
148
149 def test_operator_date_equals
150 query = Query.new(:name => '_')
151 query.add_filter('due_date', '=', ['2011-07-10'])
152 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
153 find_issues_with_query(query)
154 end
155
156 def test_operator_date_lesser_than
157 query = Query.new(:name => '_')
158 query.add_filter('due_date', '<=', ['2011-07-10'])
159 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
160 find_issues_with_query(query)
161 end
162
163 def test_operator_date_greater_than
164 query = Query.new(:name => '_')
165 query.add_filter('due_date', '>=', ['2011-07-10'])
166 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
167 find_issues_with_query(query)
168 end
169
170 def test_operator_date_between
171 query = Query.new(:name => '_')
172 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
173 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
174 find_issues_with_query(query)
175 end
176
149 def test_operator_in_more_than
177 def test_operator_in_more_than
150 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
178 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
151 query = Query.new(:project => Project.find(1), :name => '_')
179 query = Query.new(:project => Project.find(1), :name => '_')
152 query.add_filter('due_date', '>t+', ['15'])
180 query.add_filter('due_date', '>t+', ['15'])
153 issues = find_issues_with_query(query)
181 issues = find_issues_with_query(query)
154 assert !issues.empty?
182 assert !issues.empty?
155 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
183 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
156 end
184 end
157
185
158 def test_operator_in_less_than
186 def test_operator_in_less_than
159 query = Query.new(:project => Project.find(1), :name => '_')
187 query = Query.new(:project => Project.find(1), :name => '_')
160 query.add_filter('due_date', '<t+', ['15'])
188 query.add_filter('due_date', '<t+', ['15'])
161 issues = find_issues_with_query(query)
189 issues = find_issues_with_query(query)
162 assert !issues.empty?
190 assert !issues.empty?
163 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
191 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
164 end
192 end
165
193
166 def test_operator_less_than_ago
194 def test_operator_less_than_ago
167 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
195 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
168 query = Query.new(:project => Project.find(1), :name => '_')
196 query = Query.new(:project => Project.find(1), :name => '_')
169 query.add_filter('due_date', '>t-', ['3'])
197 query.add_filter('due_date', '>t-', ['3'])
170 issues = find_issues_with_query(query)
198 issues = find_issues_with_query(query)
171 assert !issues.empty?
199 assert !issues.empty?
172 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
200 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
173 end
201 end
174
202
175 def test_operator_more_than_ago
203 def test_operator_more_than_ago
176 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
204 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
177 query = Query.new(:project => Project.find(1), :name => '_')
205 query = Query.new(:project => Project.find(1), :name => '_')
178 query.add_filter('due_date', '<t-', ['10'])
206 query.add_filter('due_date', '<t-', ['10'])
179 assert query.statement.include?("#{Issue.table_name}.due_date <=")
207 assert query.statement.include?("#{Issue.table_name}.due_date <=")
180 issues = find_issues_with_query(query)
208 issues = find_issues_with_query(query)
181 assert !issues.empty?
209 assert !issues.empty?
182 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
210 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
183 end
211 end
184
212
185 def test_operator_in
213 def test_operator_in
186 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
214 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
187 query = Query.new(:project => Project.find(1), :name => '_')
215 query = Query.new(:project => Project.find(1), :name => '_')
188 query.add_filter('due_date', 't+', ['2'])
216 query.add_filter('due_date', 't+', ['2'])
189 issues = find_issues_with_query(query)
217 issues = find_issues_with_query(query)
190 assert !issues.empty?
218 assert !issues.empty?
191 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
219 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
192 end
220 end
193
221
194 def test_operator_ago
222 def test_operator_ago
195 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
223 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
196 query = Query.new(:project => Project.find(1), :name => '_')
224 query = Query.new(:project => Project.find(1), :name => '_')
197 query.add_filter('due_date', 't-', ['3'])
225 query.add_filter('due_date', 't-', ['3'])
198 issues = find_issues_with_query(query)
226 issues = find_issues_with_query(query)
199 assert !issues.empty?
227 assert !issues.empty?
200 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
228 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
201 end
229 end
202
230
203 def test_operator_today
231 def test_operator_today
204 query = Query.new(:project => Project.find(1), :name => '_')
232 query = Query.new(:project => Project.find(1), :name => '_')
205 query.add_filter('due_date', 't', [''])
233 query.add_filter('due_date', 't', [''])
206 issues = find_issues_with_query(query)
234 issues = find_issues_with_query(query)
207 assert !issues.empty?
235 assert !issues.empty?
208 issues.each {|issue| assert_equal Date.today, issue.due_date}
236 issues.each {|issue| assert_equal Date.today, issue.due_date}
209 end
237 end
210
238
211 def test_operator_this_week_on_date
239 def test_operator_this_week_on_date
212 query = Query.new(:project => Project.find(1), :name => '_')
240 query = Query.new(:project => Project.find(1), :name => '_')
213 query.add_filter('due_date', 'w', [''])
241 query.add_filter('due_date', 'w', [''])
214 find_issues_with_query(query)
242 find_issues_with_query(query)
215 end
243 end
216
244
217 def test_operator_this_week_on_datetime
245 def test_operator_this_week_on_datetime
218 query = Query.new(:project => Project.find(1), :name => '_')
246 query = Query.new(:project => Project.find(1), :name => '_')
219 query.add_filter('created_on', 'w', [''])
247 query.add_filter('created_on', 'w', [''])
220 find_issues_with_query(query)
248 find_issues_with_query(query)
221 end
249 end
222
250
223 def test_operator_contains
251 def test_operator_contains
224 query = Query.new(:project => Project.find(1), :name => '_')
252 query = Query.new(:project => Project.find(1), :name => '_')
225 query.add_filter('subject', '~', ['uNable'])
253 query.add_filter('subject', '~', ['uNable'])
226 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
254 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
227 result = find_issues_with_query(query)
255 result = find_issues_with_query(query)
228 assert result.empty?
256 assert result.empty?
229 result.each {|issue| assert issue.subject.downcase.include?('unable') }
257 result.each {|issue| assert issue.subject.downcase.include?('unable') }
230 end
258 end
231
259
232 def test_range_for_this_week_with_week_starting_on_monday
260 def test_range_for_this_week_with_week_starting_on_monday
233 I18n.locale = :fr
261 I18n.locale = :fr
234 assert_equal '1', I18n.t(:general_first_day_of_week)
262 assert_equal '1', I18n.t(:general_first_day_of_week)
235
263
236 Date.stubs(:today).returns(Date.parse('2011-04-29'))
264 Date.stubs(:today).returns(Date.parse('2011-04-29'))
237
265
238 query = Query.new(:project => Project.find(1), :name => '_')
266 query = Query.new(:project => Project.find(1), :name => '_')
239 query.add_filter('due_date', 'w', [''])
267 query.add_filter('due_date', 'w', [''])
240 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}"
268 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}"
241 I18n.locale = :en
269 I18n.locale = :en
242 end
270 end
243
271
244 def test_range_for_this_week_with_week_starting_on_sunday
272 def test_range_for_this_week_with_week_starting_on_sunday
245 I18n.locale = :en
273 I18n.locale = :en
246 assert_equal '7', I18n.t(:general_first_day_of_week)
274 assert_equal '7', I18n.t(:general_first_day_of_week)
247
275
248 Date.stubs(:today).returns(Date.parse('2011-04-29'))
276 Date.stubs(:today).returns(Date.parse('2011-04-29'))
249
277
250 query = Query.new(:project => Project.find(1), :name => '_')
278 query = Query.new(:project => Project.find(1), :name => '_')
251 query.add_filter('due_date', 'w', [''])
279 query.add_filter('due_date', 'w', [''])
252 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}"
280 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}"
253 end
281 end
254
282
255 def test_operator_does_not_contains
283 def test_operator_does_not_contains
256 query = Query.new(:project => Project.find(1), :name => '_')
284 query = Query.new(:project => Project.find(1), :name => '_')
257 query.add_filter('subject', '!~', ['uNable'])
285 query.add_filter('subject', '!~', ['uNable'])
258 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
286 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
259 find_issues_with_query(query)
287 find_issues_with_query(query)
260 end
288 end
261
289
262 def test_filter_watched_issues
290 def test_filter_watched_issues
263 User.current = User.find(1)
291 User.current = User.find(1)
264 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
292 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
265 result = find_issues_with_query(query)
293 result = find_issues_with_query(query)
266 assert_not_nil result
294 assert_not_nil result
267 assert !result.empty?
295 assert !result.empty?
268 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
296 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
269 User.current = nil
297 User.current = nil
270 end
298 end
271
299
272 def test_filter_unwatched_issues
300 def test_filter_unwatched_issues
273 User.current = User.find(1)
301 User.current = User.find(1)
274 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
302 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
275 result = find_issues_with_query(query)
303 result = find_issues_with_query(query)
276 assert_not_nil result
304 assert_not_nil result
277 assert !result.empty?
305 assert !result.empty?
278 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
306 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
279 User.current = nil
307 User.current = nil
280 end
308 end
281
309
282 def test_statement_should_be_nil_with_no_filters
310 def test_statement_should_be_nil_with_no_filters
283 q = Query.new(:name => '_')
311 q = Query.new(:name => '_')
284 q.filters = {}
312 q.filters = {}
285
313
286 assert q.valid?
314 assert q.valid?
287 assert_nil q.statement
315 assert_nil q.statement
288 end
316 end
289
317
290 def test_default_columns
318 def test_default_columns
291 q = Query.new
319 q = Query.new
292 assert !q.columns.empty?
320 assert !q.columns.empty?
293 end
321 end
294
322
295 def test_set_column_names
323 def test_set_column_names
296 q = Query.new
324 q = Query.new
297 q.column_names = ['tracker', :subject, '', 'unknonw_column']
325 q.column_names = ['tracker', :subject, '', 'unknonw_column']
298 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
326 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
299 c = q.columns.first
327 c = q.columns.first
300 assert q.has_column?(c)
328 assert q.has_column?(c)
301 end
329 end
302
330
303 def test_groupable_columns_should_include_custom_fields
331 def test_groupable_columns_should_include_custom_fields
304 q = Query.new
332 q = Query.new
305 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
333 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
306 end
334 end
307
335
308 def test_grouped_with_valid_column
336 def test_grouped_with_valid_column
309 q = Query.new(:group_by => 'status')
337 q = Query.new(:group_by => 'status')
310 assert q.grouped?
338 assert q.grouped?
311 assert_not_nil q.group_by_column
339 assert_not_nil q.group_by_column
312 assert_equal :status, q.group_by_column.name
340 assert_equal :status, q.group_by_column.name
313 assert_not_nil q.group_by_statement
341 assert_not_nil q.group_by_statement
314 assert_equal 'status', q.group_by_statement
342 assert_equal 'status', q.group_by_statement
315 end
343 end
316
344
317 def test_grouped_with_invalid_column
345 def test_grouped_with_invalid_column
318 q = Query.new(:group_by => 'foo')
346 q = Query.new(:group_by => 'foo')
319 assert !q.grouped?
347 assert !q.grouped?
320 assert_nil q.group_by_column
348 assert_nil q.group_by_column
321 assert_nil q.group_by_statement
349 assert_nil q.group_by_statement
322 end
350 end
323
351
324 def test_default_sort
352 def test_default_sort
325 q = Query.new
353 q = Query.new
326 assert_equal [], q.sort_criteria
354 assert_equal [], q.sort_criteria
327 end
355 end
328
356
329 def test_set_sort_criteria_with_hash
357 def test_set_sort_criteria_with_hash
330 q = Query.new
358 q = Query.new
331 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
359 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
332 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
360 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
333 end
361 end
334
362
335 def test_set_sort_criteria_with_array
363 def test_set_sort_criteria_with_array
336 q = Query.new
364 q = Query.new
337 q.sort_criteria = [['priority', 'desc'], 'tracker']
365 q.sort_criteria = [['priority', 'desc'], 'tracker']
338 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
366 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
339 end
367 end
340
368
341 def test_create_query_with_sort
369 def test_create_query_with_sort
342 q = Query.new(:name => 'Sorted')
370 q = Query.new(:name => 'Sorted')
343 q.sort_criteria = [['priority', 'desc'], 'tracker']
371 q.sort_criteria = [['priority', 'desc'], 'tracker']
344 assert q.save
372 assert q.save
345 q.reload
373 q.reload
346 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
374 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
347 end
375 end
348
376
349 def test_sort_by_string_custom_field_asc
377 def test_sort_by_string_custom_field_asc
350 q = Query.new
378 q = Query.new
351 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
379 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
352 assert c
380 assert c
353 assert c.sortable
381 assert c.sortable
354 issues = Issue.find :all,
382 issues = Issue.find :all,
355 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
383 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
356 :conditions => q.statement,
384 :conditions => q.statement,
357 :order => "#{c.sortable} ASC"
385 :order => "#{c.sortable} ASC"
358 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
386 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
359 assert !values.empty?
387 assert !values.empty?
360 assert_equal values.sort, values
388 assert_equal values.sort, values
361 end
389 end
362
390
363 def test_sort_by_string_custom_field_desc
391 def test_sort_by_string_custom_field_desc
364 q = Query.new
392 q = Query.new
365 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
393 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
366 assert c
394 assert c
367 assert c.sortable
395 assert c.sortable
368 issues = Issue.find :all,
396 issues = Issue.find :all,
369 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
397 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
370 :conditions => q.statement,
398 :conditions => q.statement,
371 :order => "#{c.sortable} DESC"
399 :order => "#{c.sortable} DESC"
372 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
400 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
373 assert !values.empty?
401 assert !values.empty?
374 assert_equal values.sort.reverse, values
402 assert_equal values.sort.reverse, values
375 end
403 end
376
404
377 def test_sort_by_float_custom_field_asc
405 def test_sort_by_float_custom_field_asc
378 q = Query.new
406 q = Query.new
379 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
407 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
380 assert c
408 assert c
381 assert c.sortable
409 assert c.sortable
382 issues = Issue.find :all,
410 issues = Issue.find :all,
383 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
411 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
384 :conditions => q.statement,
412 :conditions => q.statement,
385 :order => "#{c.sortable} ASC"
413 :order => "#{c.sortable} ASC"
386 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
414 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
387 assert !values.empty?
415 assert !values.empty?
388 assert_equal values.sort, values
416 assert_equal values.sort, values
389 end
417 end
390
418
391 def test_invalid_query_should_raise_query_statement_invalid_error
419 def test_invalid_query_should_raise_query_statement_invalid_error
392 q = Query.new
420 q = Query.new
393 assert_raise Query::StatementInvalid do
421 assert_raise Query::StatementInvalid do
394 q.issues(:conditions => "foo = 1")
422 q.issues(:conditions => "foo = 1")
395 end
423 end
396 end
424 end
397
425
398 def test_issue_count_by_association_group
426 def test_issue_count_by_association_group
399 q = Query.new(:name => '_', :group_by => 'assigned_to')
427 q = Query.new(:name => '_', :group_by => 'assigned_to')
400 count_by_group = q.issue_count_by_group
428 count_by_group = q.issue_count_by_group
401 assert_kind_of Hash, count_by_group
429 assert_kind_of Hash, count_by_group
402 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
430 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
403 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
431 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
404 assert count_by_group.has_key?(User.find(3))
432 assert count_by_group.has_key?(User.find(3))
405 end
433 end
406
434
407 def test_issue_count_by_list_custom_field_group
435 def test_issue_count_by_list_custom_field_group
408 q = Query.new(:name => '_', :group_by => 'cf_1')
436 q = Query.new(:name => '_', :group_by => 'cf_1')
409 count_by_group = q.issue_count_by_group
437 count_by_group = q.issue_count_by_group
410 assert_kind_of Hash, count_by_group
438 assert_kind_of Hash, count_by_group
411 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
439 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
412 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
440 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
413 assert count_by_group.has_key?('MySQL')
441 assert count_by_group.has_key?('MySQL')
414 end
442 end
415
443
416 def test_issue_count_by_date_custom_field_group
444 def test_issue_count_by_date_custom_field_group
417 q = Query.new(:name => '_', :group_by => 'cf_8')
445 q = Query.new(:name => '_', :group_by => 'cf_8')
418 count_by_group = q.issue_count_by_group
446 count_by_group = q.issue_count_by_group
419 assert_kind_of Hash, count_by_group
447 assert_kind_of Hash, count_by_group
420 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
448 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
421 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
449 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
422 end
450 end
423
451
424 def test_label_for
452 def test_label_for
425 q = Query.new
453 q = Query.new
426 assert_equal 'assigned_to', q.label_for('assigned_to_id')
454 assert_equal 'assigned_to', q.label_for('assigned_to_id')
427 end
455 end
428
456
429 def test_editable_by
457 def test_editable_by
430 admin = User.find(1)
458 admin = User.find(1)
431 manager = User.find(2)
459 manager = User.find(2)
432 developer = User.find(3)
460 developer = User.find(3)
433
461
434 # Public query on project 1
462 # Public query on project 1
435 q = Query.find(1)
463 q = Query.find(1)
436 assert q.editable_by?(admin)
464 assert q.editable_by?(admin)
437 assert q.editable_by?(manager)
465 assert q.editable_by?(manager)
438 assert !q.editable_by?(developer)
466 assert !q.editable_by?(developer)
439
467
440 # Private query on project 1
468 # Private query on project 1
441 q = Query.find(2)
469 q = Query.find(2)
442 assert q.editable_by?(admin)
470 assert q.editable_by?(admin)
443 assert !q.editable_by?(manager)
471 assert !q.editable_by?(manager)
444 assert q.editable_by?(developer)
472 assert q.editable_by?(developer)
445
473
446 # Private query for all projects
474 # Private query for all projects
447 q = Query.find(3)
475 q = Query.find(3)
448 assert q.editable_by?(admin)
476 assert q.editable_by?(admin)
449 assert !q.editable_by?(manager)
477 assert !q.editable_by?(manager)
450 assert q.editable_by?(developer)
478 assert q.editable_by?(developer)
451
479
452 # Public query for all projects
480 # Public query for all projects
453 q = Query.find(4)
481 q = Query.find(4)
454 assert q.editable_by?(admin)
482 assert q.editable_by?(admin)
455 assert !q.editable_by?(manager)
483 assert !q.editable_by?(manager)
456 assert !q.editable_by?(developer)
484 assert !q.editable_by?(developer)
457 end
485 end
458
486
459 def test_visible_scope
487 def test_visible_scope
460 query_ids = Query.visible(User.anonymous).map(&:id)
488 query_ids = Query.visible(User.anonymous).map(&:id)
461
489
462 assert query_ids.include?(1), 'public query on public project was not visible'
490 assert query_ids.include?(1), 'public query on public project was not visible'
463 assert query_ids.include?(4), 'public query for all projects was not visible'
491 assert query_ids.include?(4), 'public query for all projects was not visible'
464 assert !query_ids.include?(2), 'private query on public project was visible'
492 assert !query_ids.include?(2), 'private query on public project was visible'
465 assert !query_ids.include?(3), 'private query for all projects was visible'
493 assert !query_ids.include?(3), 'private query for all projects was visible'
466 assert !query_ids.include?(7), 'public query on private project was visible'
494 assert !query_ids.include?(7), 'public query on private project was visible'
467 end
495 end
468
496
469 context "#available_filters" do
497 context "#available_filters" do
470 setup do
498 setup do
471 @query = Query.new(:name => "_")
499 @query = Query.new(:name => "_")
472 end
500 end
473
501
474 should "include users of visible projects in cross-project view" do
502 should "include users of visible projects in cross-project view" do
475 users = @query.available_filters["assigned_to_id"]
503 users = @query.available_filters["assigned_to_id"]
476 assert_not_nil users
504 assert_not_nil users
477 assert users[:values].map{|u|u[1]}.include?("3")
505 assert users[:values].map{|u|u[1]}.include?("3")
478 end
506 end
479
507
480 should "include visible projects in cross-project view" do
508 should "include visible projects in cross-project view" do
481 projects = @query.available_filters["project_id"]
509 projects = @query.available_filters["project_id"]
482 assert_not_nil projects
510 assert_not_nil projects
483 assert projects[:values].map{|u|u[1]}.include?("1")
511 assert projects[:values].map{|u|u[1]}.include?("1")
484 end
512 end
485
513
486 context "'member_of_group' filter" do
514 context "'member_of_group' filter" do
487 should "be present" do
515 should "be present" do
488 assert @query.available_filters.keys.include?("member_of_group")
516 assert @query.available_filters.keys.include?("member_of_group")
489 end
517 end
490
518
491 should "be an optional list" do
519 should "be an optional list" do
492 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
520 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
493 end
521 end
494
522
495 should "have a list of the groups as values" do
523 should "have a list of the groups as values" do
496 Group.destroy_all # No fixtures
524 Group.destroy_all # No fixtures
497 group1 = Group.generate!.reload
525 group1 = Group.generate!.reload
498 group2 = Group.generate!.reload
526 group2 = Group.generate!.reload
499
527
500 expected_group_list = [
528 expected_group_list = [
501 [group1.name, group1.id.to_s],
529 [group1.name, group1.id.to_s],
502 [group2.name, group2.id.to_s]
530 [group2.name, group2.id.to_s]
503 ]
531 ]
504 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
532 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
505 end
533 end
506
534
507 end
535 end
508
536
509 context "'assigned_to_role' filter" do
537 context "'assigned_to_role' filter" do
510 should "be present" do
538 should "be present" do
511 assert @query.available_filters.keys.include?("assigned_to_role")
539 assert @query.available_filters.keys.include?("assigned_to_role")
512 end
540 end
513
541
514 should "be an optional list" do
542 should "be an optional list" do
515 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
543 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
516 end
544 end
517
545
518 should "have a list of the Roles as values" do
546 should "have a list of the Roles as values" do
519 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
547 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
520 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
548 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
521 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
549 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
522 end
550 end
523
551
524 should "not include the built in Roles as values" do
552 should "not include the built in Roles as values" do
525 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
553 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
526 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
554 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
527 end
555 end
528
556
529 end
557 end
530
558
531 end
559 end
532
560
533 context "#statement" do
561 context "#statement" do
534 context "with 'member_of_group' filter" do
562 context "with 'member_of_group' filter" do
535 setup do
563 setup do
536 Group.destroy_all # No fixtures
564 Group.destroy_all # No fixtures
537 @user_in_group = User.generate!
565 @user_in_group = User.generate!
538 @second_user_in_group = User.generate!
566 @second_user_in_group = User.generate!
539 @user_in_group2 = User.generate!
567 @user_in_group2 = User.generate!
540 @user_not_in_group = User.generate!
568 @user_not_in_group = User.generate!
541
569
542 @group = Group.generate!.reload
570 @group = Group.generate!.reload
543 @group.users << @user_in_group
571 @group.users << @user_in_group
544 @group.users << @second_user_in_group
572 @group.users << @second_user_in_group
545
573
546 @group2 = Group.generate!.reload
574 @group2 = Group.generate!.reload
547 @group2.users << @user_in_group2
575 @group2.users << @user_in_group2
548
576
549 end
577 end
550
578
551 should "search assigned to for users in the group" do
579 should "search assigned to for users in the group" do
552 @query = Query.new(:name => '_')
580 @query = Query.new(:name => '_')
553 @query.add_filter('member_of_group', '=', [@group.id.to_s])
581 @query.add_filter('member_of_group', '=', [@group.id.to_s])
554
582
555 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
583 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
556 assert_find_issues_with_query_is_successful @query
584 assert_find_issues_with_query_is_successful @query
557 end
585 end
558
586
559 should "search not assigned to any group member (none)" do
587 should "search not assigned to any group member (none)" do
560 @query = Query.new(:name => '_')
588 @query = Query.new(:name => '_')
561 @query.add_filter('member_of_group', '!*', [''])
589 @query.add_filter('member_of_group', '!*', [''])
562
590
563 # Users not in a group
591 # Users not in a group
564 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}')"
592 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}')"
565 assert_find_issues_with_query_is_successful @query
593 assert_find_issues_with_query_is_successful @query
566 end
594 end
567
595
568 should "search assigned to any group member (all)" do
596 should "search assigned to any group member (all)" do
569 @query = Query.new(:name => '_')
597 @query = Query.new(:name => '_')
570 @query.add_filter('member_of_group', '*', [''])
598 @query.add_filter('member_of_group', '*', [''])
571
599
572 # Only users in a group
600 # Only users in a group
573 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}')"
601 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}')"
574 assert_find_issues_with_query_is_successful @query
602 assert_find_issues_with_query_is_successful @query
575 end
603 end
576
604
577 should "return an empty set with = empty group" do
605 should "return an empty set with = empty group" do
578 @empty_group = Group.generate!
606 @empty_group = Group.generate!
579 @query = Query.new(:name => '_')
607 @query = Query.new(:name => '_')
580 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
608 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
581
609
582 assert_equal [], find_issues_with_query(@query)
610 assert_equal [], find_issues_with_query(@query)
583 end
611 end
584
612
585 should "return issues with ! empty group" do
613 should "return issues with ! empty group" do
586 @empty_group = Group.generate!
614 @empty_group = Group.generate!
587 @query = Query.new(:name => '_')
615 @query = Query.new(:name => '_')
588 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
616 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
589
617
590 assert_find_issues_with_query_is_successful @query
618 assert_find_issues_with_query_is_successful @query
591 end
619 end
592 end
620 end
593
621
594 context "with 'assigned_to_role' filter" do
622 context "with 'assigned_to_role' filter" do
595 setup do
623 setup do
596 # No fixtures
624 # No fixtures
597 MemberRole.delete_all
625 MemberRole.delete_all
598 Member.delete_all
626 Member.delete_all
599 Role.delete_all
627 Role.delete_all
600
628
601 @manager_role = Role.generate!(:name => 'Manager')
629 @manager_role = Role.generate!(:name => 'Manager')
602 @developer_role = Role.generate!(:name => 'Developer')
630 @developer_role = Role.generate!(:name => 'Developer')
603
631
604 @project = Project.generate!
632 @project = Project.generate!
605 @manager = User.generate!
633 @manager = User.generate!
606 @developer = User.generate!
634 @developer = User.generate!
607 @boss = User.generate!
635 @boss = User.generate!
608 User.add_to_project(@manager, @project, @manager_role)
636 User.add_to_project(@manager, @project, @manager_role)
609 User.add_to_project(@developer, @project, @developer_role)
637 User.add_to_project(@developer, @project, @developer_role)
610 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
638 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
611 end
639 end
612
640
613 should "search assigned to for users with the Role" do
641 should "search assigned to for users with the Role" do
614 @query = Query.new(:name => '_')
642 @query = Query.new(:name => '_')
615 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
643 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
616
644
617 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')"
645 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')"
618 assert_find_issues_with_query_is_successful @query
646 assert_find_issues_with_query_is_successful @query
619 end
647 end
620
648
621 should "search assigned to for users not assigned to any Role (none)" do
649 should "search assigned to for users not assigned to any Role (none)" do
622 @query = Query.new(:name => '_')
650 @query = Query.new(:name => '_')
623 @query.add_filter('assigned_to_role', '!*', [''])
651 @query.add_filter('assigned_to_role', '!*', [''])
624
652
625 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
653 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
626 assert_find_issues_with_query_is_successful @query
654 assert_find_issues_with_query_is_successful @query
627 end
655 end
628
656
629 should "search assigned to for users assigned to any Role (all)" do
657 should "search assigned to for users assigned to any Role (all)" do
630 @query = Query.new(:name => '_')
658 @query = Query.new(:name => '_')
631 @query.add_filter('assigned_to_role', '*', [''])
659 @query.add_filter('assigned_to_role', '*', [''])
632
660
633 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
661 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
634 assert_find_issues_with_query_is_successful @query
662 assert_find_issues_with_query_is_successful @query
635 end
663 end
636
664
637 should "return an empty set with empty role" do
665 should "return an empty set with empty role" do
638 @empty_role = Role.generate!
666 @empty_role = Role.generate!
639 @query = Query.new(:name => '_')
667 @query = Query.new(:name => '_')
640 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
668 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
641
669
642 assert_equal [], find_issues_with_query(@query)
670 assert_equal [], find_issues_with_query(@query)
643 end
671 end
644
672
645 should "return issues with ! empty role" do
673 should "return issues with ! empty role" do
646 @empty_role = Role.generate!
674 @empty_role = Role.generate!
647 @query = Query.new(:name => '_')
675 @query = Query.new(:name => '_')
648 @query.add_filter('member_of_group', '!', [@empty_role.id.to_s])
676 @query.add_filter('member_of_group', '!', [@empty_role.id.to_s])
649
677
650 assert_find_issues_with_query_is_successful @query
678 assert_find_issues_with_query_is_successful @query
651 end
679 end
652 end
680 end
653 end
681 end
654
682
655 end
683 end
General Comments 0
You need to be logged in to leave comments. Login now