##// END OF EJS Templates
Don't preload all query filters (#24787)....
Jean-Philippe Lang -
r15788:fd3c08aaa107
parent child
Show More
@@ -1,146 +1,165
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueriesController < ApplicationController
19 19 menu_item :issues
20 before_action :find_query, :except => [:new, :create, :index]
20 before_action :find_query, :only => [:edit, :update, :destroy]
21 21 before_action :find_optional_project, :only => [:new, :create]
22 22
23 23 accept_api_auth :index
24 24
25 25 include QueriesHelper
26 26
27 27 def index
28 28 case params[:format]
29 29 when 'xml', 'json'
30 30 @offset, @limit = api_offset_and_limit
31 31 else
32 32 @limit = per_page_option
33 33 end
34 34 scope = query_class.visible
35 35 @query_count = scope.count
36 36 @query_pages = Paginator.new @query_count, @limit, params['page']
37 37 @queries = scope.
38 38 order("#{Query.table_name}.name").
39 39 limit(@limit).
40 40 offset(@offset).
41 41 to_a
42 42 respond_to do |format|
43 43 format.html {render_error :status => 406}
44 44 format.api
45 45 end
46 46 end
47 47
48 48 def new
49 49 @query = query_class.new
50 50 @query.user = User.current
51 51 @query.project = @project
52 52 @query.build_from_params(params)
53 53 end
54 54
55 55 def create
56 56 @query = query_class.new
57 57 @query.user = User.current
58 58 @query.project = @project
59 59 update_query_from_params
60 60
61 61 if @query.save
62 62 flash[:notice] = l(:notice_successful_create)
63 63 redirect_to_items(:query_id => @query)
64 64 else
65 65 render :action => 'new', :layout => !request.xhr?
66 66 end
67 67 end
68 68
69 69 def edit
70 70 end
71 71
72 72 def update
73 73 update_query_from_params
74 74
75 75 if @query.save
76 76 flash[:notice] = l(:notice_successful_update)
77 77 redirect_to_items(:query_id => @query)
78 78 else
79 79 render :action => 'edit'
80 80 end
81 81 end
82 82
83 83 def destroy
84 84 @query.destroy
85 85 redirect_to_items(:set_filter => 1)
86 86 end
87 87
88 # Returns the values for a query filter
89 def filter
90 q = query_class.new
91 if params[:project_id].present?
92 q.project = Project.find(params[:project_id])
93 end
94
95 unless User.current.allowed_to?(q.class.view_permission, q.project, :global => true)
96 raise Unauthorized
97 end
98
99 filter = q.available_filters[params[:name].to_s]
100 values = filter ? filter.values : []
101
102 render :json => values
103 rescue ActiveRecord::RecordNotFound
104 render_404
105 end
106
88 107 private
89 108
90 109 def find_query
91 110 @query = Query.find(params[:id])
92 111 @project = @query.project
93 112 render_403 unless @query.editable_by?(User.current)
94 113 rescue ActiveRecord::RecordNotFound
95 114 render_404
96 115 end
97 116
98 117 def find_optional_project
99 118 @project = Project.find(params[:project_id]) if params[:project_id]
100 119 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
101 120 rescue ActiveRecord::RecordNotFound
102 121 render_404
103 122 end
104 123
105 124 def update_query_from_params
106 125 @query.project = params[:query_is_for_all] ? nil : @project
107 126 @query.build_from_params(params)
108 127 @query.column_names = nil if params[:default_columns]
109 128 @query.sort_criteria = params[:query] && params[:query][:sort_criteria]
110 129 @query.name = params[:query] && params[:query][:name]
111 130 if User.current.allowed_to?(:manage_public_queries, @query.project) || User.current.admin?
112 131 @query.visibility = (params[:query] && params[:query][:visibility]) || Query::VISIBILITY_PRIVATE
113 132 @query.role_ids = params[:query] && params[:query][:role_ids]
114 133 else
115 134 @query.visibility = Query::VISIBILITY_PRIVATE
116 135 end
117 136 @query
118 137 end
119 138
120 139 def redirect_to_items(options)
121 140 method = "redirect_to_#{@query.class.name.underscore}"
122 141 send method, options
123 142 end
124 143
125 144 def redirect_to_issue_query(options)
126 145 if params[:gantt]
127 146 if @project
128 147 redirect_to project_gantt_path(@project, options)
129 148 else
130 149 redirect_to issues_gantt_path(options)
131 150 end
132 151 else
133 152 redirect_to _project_issues_path(@project, options)
134 153 end
135 154 end
136 155
137 156 def redirect_to_time_entry_query(options)
138 157 redirect_to _time_entries_path(@project, nil, options)
139 158 end
140 159
141 160 # Returns the Query subclass, IssueQuery by default
142 161 # for compatibility with previous behaviour
143 162 def query_class
144 163 Query.get_subclass(params[:type] || 'IssueQuery')
145 164 end
146 165 end
@@ -1,549 +1,508
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueQuery < Query
19 19
20 20 self.queried_class = Issue
21 21 self.view_permission = :view_issues
22 22
23 23 self.available_columns = [
24 24 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
25 25 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
26 26 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
27 27 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
28 28 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
29 29 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
30 30 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
31 31 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
32 32 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
33 33 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
34 34 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
35 35 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
36 36 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
37 37 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
38 38 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true),
39 39 QueryColumn.new(:total_estimated_hours,
40 40 :sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
41 41 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
42 42 :default_order => 'desc'),
43 43 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
44 44 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
45 45 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
46 46 QueryColumn.new(:relations, :caption => :label_related_issues),
47 47 QueryColumn.new(:description, :inline => false)
48 48 ]
49 49
50 50 def initialize(attributes=nil, *args)
51 51 super attributes
52 52 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
53 53 end
54 54
55 55 def draw_relations
56 56 r = options[:draw_relations]
57 57 r.nil? || r == '1'
58 58 end
59 59
60 60 def draw_relations=(arg)
61 61 options[:draw_relations] = (arg == '0' ? '0' : nil)
62 62 end
63 63
64 64 def draw_progress_line
65 65 r = options[:draw_progress_line]
66 66 r == '1'
67 67 end
68 68
69 69 def draw_progress_line=(arg)
70 70 options[:draw_progress_line] = (arg == '1' ? '1' : nil)
71 71 end
72 72
73 73 def build_from_params(params)
74 74 super
75 75 self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
76 76 self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
77 77 self
78 78 end
79 79
80 80 def initialize_available_filters
81 principals = []
82 subprojects = []
83 versions = []
84 categories = []
85 issue_custom_fields = []
86
87 if project
88 principals += project.principals.visible
89 unless project.leaf?
90 subprojects = project.descendants.visible.to_a
91 principals += Principal.member_of(subprojects).visible
92 end
93 versions = project.shared_versions.to_a
94 categories = project.issue_categories.to_a
95 issue_custom_fields = project.all_issue_custom_fields
96 else
97 if all_projects.any?
98 principals += Principal.member_of(all_projects).visible
99 end
100 versions = Version.visible.where(:sharing => 'system').to_a
101 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
102 end
103 principals.uniq!
104 principals.sort!
105 principals.reject! {|p| p.is_a?(GroupBuiltin)}
106 users = principals.select {|p| p.is_a?(User)}
107
108 81 add_available_filter "status_id",
109 :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] }
82 :type => :list_status, :values => lambda { IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] } }
110 83
111 if project.nil?
112 project_values = []
113 if User.current.logged? && User.current.memberships.any?
114 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
115 end
116 project_values += all_projects_values
117 add_available_filter("project_id",
118 :type => :list, :values => project_values
119 ) unless project_values.empty?
120 end
84 add_available_filter("project_id",
85 :type => :list, :values => lambda { project_values }
86 ) if project.nil?
121 87
122 88 add_available_filter "tracker_id",
123 89 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
90
124 91 add_available_filter "priority_id",
125 92 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
126 93
127 author_values = []
128 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
129 author_values += users.collect{|s| [s.name, s.id.to_s] }
130 94 add_available_filter("author_id",
131 :type => :list, :values => author_values
132 ) unless author_values.empty?
95 :type => :list, :values => lambda { author_values }
96 )
133 97
134 assigned_to_values = []
135 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
136 assigned_to_values += (Setting.issue_group_assignment? ?
137 principals : users).collect{|s| [s.name, s.id.to_s] }
138 98 add_available_filter("assigned_to_id",
139 :type => :list_optional, :values => assigned_to_values
140 ) unless assigned_to_values.empty?
99 :type => :list_optional, :values => lambda { assigned_to_values }
100 )
141 101
142 group_values = Group.givable.visible.collect {|g| [g.name, g.id.to_s] }
143 102 add_available_filter("member_of_group",
144 :type => :list_optional, :values => group_values
145 ) unless group_values.empty?
103 :type => :list_optional, :values => lambda { Group.givable.visible.collect {|g| [g.name, g.id.to_s] } }
104 )
146 105
147 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
148 106 add_available_filter("assigned_to_role",
149 :type => :list_optional, :values => role_values
150 ) unless role_values.empty?
107 :type => :list_optional, :values => lambda { Role.givable.collect {|r| [r.name, r.id.to_s] } }
108 )
151 109
152 110 add_available_filter "fixed_version_id",
153 :type => :list_optional,
154 :values => Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] }
111 :type => :list_optional, :values => lambda { fixed_version_values }
155 112
156 113 add_available_filter "fixed_version.due_date",
157 114 :type => :date,
158 115 :name => l(:label_attribute_of_fixed_version, :name => l(:field_effective_date))
159 116
160 117 add_available_filter "fixed_version.status",
161 118 :type => :list,
162 119 :name => l(:label_attribute_of_fixed_version, :name => l(:field_status)),
163 120 :values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s] }
164 121
165 122 add_available_filter "category_id",
166 123 :type => :list_optional,
167 :values => categories.collect{|s| [s.name, s.id.to_s] }
124 :values => lambda { project.issue_categories.collect{|s| [s.name, s.id.to_s] } } if project
168 125
169 126 add_available_filter "subject", :type => :text
170 127 add_available_filter "description", :type => :text
171 128 add_available_filter "created_on", :type => :date_past
172 129 add_available_filter "updated_on", :type => :date_past
173 130 add_available_filter "closed_on", :type => :date_past
174 131 add_available_filter "start_date", :type => :date
175 132 add_available_filter "due_date", :type => :date
176 133 add_available_filter "estimated_hours", :type => :float
177 134 add_available_filter "done_ratio", :type => :integer
178 135
179 136 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
180 137 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
181 138 add_available_filter "is_private",
182 139 :type => :list,
183 140 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
184 141 end
185 142
186 143 if User.current.logged?
187 144 add_available_filter "watcher_id",
188 145 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
189 146 end
190 147
191 if subprojects.any?
148 if project && !project.leaf?
192 149 add_available_filter "subproject_id",
193 150 :type => :list_subprojects,
194 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
151 :values => lambda { subproject_values }
195 152 end
196 153
154
155 issue_custom_fields = project ? project.all_issue_custom_fields : IssueCustomField.where(:is_for_all => true)
197 156 add_custom_fields_filters(issue_custom_fields)
198 157
199 158 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
200 159
201 160 IssueRelation::TYPES.each do |relation_type, options|
202 add_available_filter relation_type, :type => :relation, :label => options[:name]
161 add_available_filter relation_type, :type => :relation, :label => options[:name], :values => lambda {all_projects_values}
203 162 end
204 163 add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue
205 164 add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural
206 165
207 166 add_available_filter "issue_id", :type => :integer, :label => :label_issue
208 167
209 168 Tracker.disabled_core_fields(trackers).each {|field|
210 169 delete_available_filter field
211 170 }
212 171 end
213 172
214 173 def available_columns
215 174 return @available_columns if @available_columns
216 175 @available_columns = self.class.available_columns.dup
217 176 @available_columns += (project ?
218 177 project.all_issue_custom_fields :
219 178 IssueCustomField
220 179 ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
221 180
222 181 if User.current.allowed_to?(:view_time_entries, project, :global => true)
223 182 index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
224 183 index = (index ? index + 1 : -1)
225 184 # insert the column after total_estimated_hours or at the end
226 185 @available_columns.insert index, QueryColumn.new(:spent_hours,
227 186 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
228 187 :default_order => 'desc',
229 188 :caption => :label_spent_time,
230 189 :totalable => true
231 190 )
232 191 @available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
233 192 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" +
234 193 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
235 194 :default_order => 'desc',
236 195 :caption => :label_total_spent_time
237 196 )
238 197 end
239 198
240 199 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
241 200 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
242 201 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
243 202 end
244 203
245 204 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
246 205 @available_columns.reject! {|column|
247 206 disabled_fields.include?(column.name.to_s)
248 207 }
249 208
250 209 @available_columns
251 210 end
252 211
253 212 def default_columns_names
254 213 @default_columns_names ||= begin
255 214 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
256 215
257 216 project.present? ? default_columns : [:project] | default_columns
258 217 end
259 218 end
260 219
261 220 def default_totalable_names
262 221 Setting.issue_list_default_totals.map(&:to_sym)
263 222 end
264 223
265 224 def base_scope
266 225 Issue.visible.joins(:status, :project).where(statement)
267 226 end
268 227
269 228 # Returns the issue count
270 229 def issue_count
271 230 base_scope.count
272 231 rescue ::ActiveRecord::StatementInvalid => e
273 232 raise StatementInvalid.new(e.message)
274 233 end
275 234
276 235 # Returns the issue count by group or nil if query is not grouped
277 236 def issue_count_by_group
278 237 grouped_query do |scope|
279 238 scope.count
280 239 end
281 240 end
282 241
283 242 # Returns sum of all the issue's estimated_hours
284 243 def total_for_estimated_hours(scope)
285 244 map_total(scope.sum(:estimated_hours)) {|t| t.to_f.round(2)}
286 245 end
287 246
288 247 # Returns sum of all the issue's time entries hours
289 248 def total_for_spent_hours(scope)
290 249 total = if group_by_column.try(:name) == :project
291 250 # TODO: remove this when https://github.com/rails/rails/issues/21922 is fixed
292 251 # We have to do a custom join without the time_entries.project_id column
293 252 # that would trigger a ambiguous column name error
294 253 scope.joins("JOIN (SELECT issue_id, hours FROM #{TimeEntry.table_name}) AS joined_time_entries ON joined_time_entries.issue_id = #{Issue.table_name}.id").
295 254 sum("joined_time_entries.hours")
296 255 else
297 256 scope.joins(:time_entries).sum("#{TimeEntry.table_name}.hours")
298 257 end
299 258 map_total(total) {|t| t.to_f.round(2)}
300 259 end
301 260
302 261 # Returns the issues
303 262 # Valid options are :order, :offset, :limit, :include, :conditions
304 263 def issues(options={})
305 264 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
306 265
307 266 scope = Issue.visible.
308 267 joins(:status, :project).
309 268 where(statement).
310 269 includes(([:status, :project] + (options[:include] || [])).uniq).
311 270 where(options[:conditions]).
312 271 order(order_option).
313 272 joins(joins_for_order_statement(order_option.join(','))).
314 273 limit(options[:limit]).
315 274 offset(options[:offset])
316 275
317 276 scope = scope.preload(:custom_values)
318 277 if has_column?(:author)
319 278 scope = scope.preload(:author)
320 279 end
321 280
322 281 issues = scope.to_a
323 282
324 283 if has_column?(:spent_hours)
325 284 Issue.load_visible_spent_hours(issues)
326 285 end
327 286 if has_column?(:total_spent_hours)
328 287 Issue.load_visible_total_spent_hours(issues)
329 288 end
330 289 if has_column?(:relations)
331 290 Issue.load_visible_relations(issues)
332 291 end
333 292 issues
334 293 rescue ::ActiveRecord::StatementInvalid => e
335 294 raise StatementInvalid.new(e.message)
336 295 end
337 296
338 297 # Returns the issues ids
339 298 def issue_ids(options={})
340 299 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
341 300
342 301 Issue.visible.
343 302 joins(:status, :project).
344 303 where(statement).
345 304 includes(([:status, :project] + (options[:include] || [])).uniq).
346 305 references(([:status, :project] + (options[:include] || [])).uniq).
347 306 where(options[:conditions]).
348 307 order(order_option).
349 308 joins(joins_for_order_statement(order_option.join(','))).
350 309 limit(options[:limit]).
351 310 offset(options[:offset]).
352 311 pluck(:id)
353 312 rescue ::ActiveRecord::StatementInvalid => e
354 313 raise StatementInvalid.new(e.message)
355 314 end
356 315
357 316 # Returns the journals
358 317 # Valid options are :order, :offset, :limit
359 318 def journals(options={})
360 319 Journal.visible.
361 320 joins(:issue => [:project, :status]).
362 321 where(statement).
363 322 order(options[:order]).
364 323 limit(options[:limit]).
365 324 offset(options[:offset]).
366 325 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
367 326 to_a
368 327 rescue ::ActiveRecord::StatementInvalid => e
369 328 raise StatementInvalid.new(e.message)
370 329 end
371 330
372 331 # Returns the versions
373 332 # Valid options are :conditions
374 333 def versions(options={})
375 334 Version.visible.
376 335 where(project_statement).
377 336 where(options[:conditions]).
378 337 includes(:project).
379 338 references(:project).
380 339 to_a
381 340 rescue ::ActiveRecord::StatementInvalid => e
382 341 raise StatementInvalid.new(e.message)
383 342 end
384 343
385 344 def sql_for_watcher_id_field(field, operator, value)
386 345 db_table = Watcher.table_name
387 346 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
388 347 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
389 348 end
390 349
391 350 def sql_for_member_of_group_field(field, operator, value)
392 351 if operator == '*' # Any group
393 352 groups = Group.givable
394 353 operator = '=' # Override the operator since we want to find by assigned_to
395 354 elsif operator == "!*"
396 355 groups = Group.givable
397 356 operator = '!' # Override the operator since we want to find by assigned_to
398 357 else
399 358 groups = Group.where(:id => value).to_a
400 359 end
401 360 groups ||= []
402 361
403 362 members_of_groups = groups.inject([]) {|user_ids, group|
404 363 user_ids + group.user_ids + [group.id]
405 364 }.uniq.compact.sort.collect(&:to_s)
406 365
407 366 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
408 367 end
409 368
410 369 def sql_for_assigned_to_role_field(field, operator, value)
411 370 case operator
412 371 when "*", "!*" # Member / Not member
413 372 sw = operator == "!*" ? 'NOT' : ''
414 373 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
415 374 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
416 375 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
417 376 when "=", "!"
418 377 role_cond = value.any? ?
419 378 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")" :
420 379 "1=0"
421 380
422 381 sw = operator == "!" ? 'NOT' : ''
423 382 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
424 383 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
425 384 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
426 385 end
427 386 end
428 387
429 388 def sql_for_fixed_version_status_field(field, operator, value)
430 389 where = sql_for_field(field, operator, value, Version.table_name, "status")
431 390 version_ids = versions(:conditions => [where]).map(&:id)
432 391
433 392 nl = operator == "!" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
434 393 "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
435 394 end
436 395
437 396 def sql_for_fixed_version_due_date_field(field, operator, value)
438 397 where = sql_for_field(field, operator, value, Version.table_name, "effective_date")
439 398 version_ids = versions(:conditions => [where]).map(&:id)
440 399
441 400 nl = operator == "!*" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
442 401 "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
443 402 end
444 403
445 404 def sql_for_is_private_field(field, operator, value)
446 405 op = (operator == "=" ? 'IN' : 'NOT IN')
447 406 va = value.map {|v| v == '0' ? self.class.connection.quoted_false : self.class.connection.quoted_true}.uniq.join(',')
448 407
449 408 "#{Issue.table_name}.is_private #{op} (#{va})"
450 409 end
451 410
452 411 def sql_for_parent_id_field(field, operator, value)
453 412 case operator
454 413 when "="
455 414 "#{Issue.table_name}.parent_id = #{value.first.to_i}"
456 415 when "~"
457 416 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
458 417 if root_id && lft && rgt
459 418 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft > #{lft} AND #{Issue.table_name}.rgt < #{rgt}"
460 419 else
461 420 "1=0"
462 421 end
463 422 when "!*"
464 423 "#{Issue.table_name}.parent_id IS NULL"
465 424 when "*"
466 425 "#{Issue.table_name}.parent_id IS NOT NULL"
467 426 end
468 427 end
469 428
470 429 def sql_for_child_id_field(field, operator, value)
471 430 case operator
472 431 when "="
473 432 parent_id = Issue.where(:id => value.first.to_i).pluck(:parent_id).first
474 433 if parent_id
475 434 "#{Issue.table_name}.id = #{parent_id}"
476 435 else
477 436 "1=0"
478 437 end
479 438 when "~"
480 439 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
481 440 if root_id && lft && rgt
482 441 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft < #{lft} AND #{Issue.table_name}.rgt > #{rgt}"
483 442 else
484 443 "1=0"
485 444 end
486 445 when "!*"
487 446 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1"
488 447 when "*"
489 448 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft > 1"
490 449 end
491 450 end
492 451
493 452 def sql_for_issue_id_field(field, operator, value)
494 453 if operator == "="
495 454 # accepts a comma separated list of ids
496 455 ids = value.first.to_s.scan(/\d+/).map(&:to_i)
497 456 if ids.present?
498 457 "#{Issue.table_name}.id IN (#{ids.join(",")})"
499 458 else
500 459 "1=0"
501 460 end
502 461 else
503 462 sql_for_field("id", operator, value, Issue.table_name, "id")
504 463 end
505 464 end
506 465
507 466 def sql_for_relations(field, operator, value, options={})
508 467 relation_options = IssueRelation::TYPES[field]
509 468 return relation_options unless relation_options
510 469
511 470 relation_type = field
512 471 join_column, target_join_column = "issue_from_id", "issue_to_id"
513 472 if relation_options[:reverse] || options[:reverse]
514 473 relation_type = relation_options[:reverse] || relation_type
515 474 join_column, target_join_column = target_join_column, join_column
516 475 end
517 476
518 477 sql = case operator
519 478 when "*", "!*"
520 479 op = (operator == "*" ? 'IN' : 'NOT IN')
521 480 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}')"
522 481 when "=", "!"
523 482 op = (operator == "=" ? 'IN' : 'NOT IN')
524 483 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
525 484 when "=p", "=!p", "!p"
526 485 op = (operator == "!p" ? 'NOT IN' : 'IN')
527 486 comp = (operator == "=!p" ? '<>' : '=')
528 487 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
529 488 when "*o", "!o"
530 489 op = (operator == "!o" ? 'NOT IN' : 'IN')
531 490 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false}))"
532 491 end
533 492
534 493 if relation_options[:sym] == field && !options[:reverse]
535 494 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
536 495 sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
537 496 end
538 497 "(#{sql})"
539 498 end
540 499
541 500 def find_assigned_to_id_filter_values(values)
542 501 Principal.visible.where(:id => values).map {|p| [p.name, p.id.to_s]}
543 502 end
544 503 alias :find_author_id_filter_values :find_assigned_to_id_filter_values
545 504
546 505 IssueRelation::TYPES.keys.each do |relation_type|
547 506 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
548 507 end
549 508 end
@@ -1,1136 +1,1231
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :totalable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.totalable = options[:totalable] || false
30 30 self.default_order = options[:default_order]
31 31 @inline = options.key?(:inline) ? options[:inline] : true
32 32 @caption_key = options[:caption] || "field_#{name}".to_sym
33 33 @frozen = options[:frozen]
34 34 end
35 35
36 36 def caption
37 37 case @caption_key
38 38 when Symbol
39 39 l(@caption_key)
40 40 when Proc
41 41 @caption_key.call
42 42 else
43 43 @caption_key
44 44 end
45 45 end
46 46
47 47 # Returns true if the column is sortable, otherwise false
48 48 def sortable?
49 49 !@sortable.nil?
50 50 end
51 51
52 52 def sortable
53 53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
54 54 end
55 55
56 56 def inline?
57 57 @inline
58 58 end
59 59
60 60 def frozen?
61 61 @frozen
62 62 end
63 63
64 64 def value(object)
65 65 object.send name
66 66 end
67 67
68 68 def value_object(object)
69 69 object.send name
70 70 end
71 71
72 72 def css_classes
73 73 name
74 74 end
75 75 end
76 76
77 77 class QueryAssociationColumn < QueryColumn
78 78
79 79 def initialize(association, attribute, options={})
80 80 @association = association
81 81 @attribute = attribute
82 82 name_with_assoc = "#{association}.#{attribute}".to_sym
83 83 super(name_with_assoc, options)
84 84 end
85 85
86 86 def value_object(object)
87 87 if assoc = object.send(@association)
88 88 assoc.send @attribute
89 89 end
90 90 end
91 91
92 92 def css_classes
93 93 @css_classes ||= "#{@association}-#{@attribute}"
94 94 end
95 95 end
96 96
97 97 class QueryCustomFieldColumn < QueryColumn
98 98
99 99 def initialize(custom_field, options={})
100 100 self.name = "cf_#{custom_field.id}".to_sym
101 101 self.sortable = custom_field.order_statement || false
102 102 self.groupable = custom_field.group_statement || false
103 103 self.totalable = options.key?(:totalable) ? !!options[:totalable] : custom_field.totalable?
104 104 @inline = true
105 105 @cf = custom_field
106 106 end
107 107
108 108 def caption
109 109 @cf.name
110 110 end
111 111
112 112 def custom_field
113 113 @cf
114 114 end
115 115
116 116 def value_object(object)
117 117 if custom_field.visible_by?(object.project, User.current)
118 118 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
119 119 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
120 120 else
121 121 nil
122 122 end
123 123 end
124 124
125 125 def value(object)
126 126 raw = value_object(object)
127 127 if raw.is_a?(Array)
128 128 raw.map {|r| @cf.cast_value(r.value)}
129 129 elsif raw
130 130 @cf.cast_value(raw.value)
131 131 else
132 132 nil
133 133 end
134 134 end
135 135
136 136 def css_classes
137 137 @css_classes ||= "#{name} #{@cf.field_format}"
138 138 end
139 139 end
140 140
141 141 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
142 142
143 143 def initialize(association, custom_field, options={})
144 144 super(custom_field, options)
145 145 self.name = "#{association}.cf_#{custom_field.id}".to_sym
146 146 # TODO: support sorting/grouping by association custom field
147 147 self.sortable = false
148 148 self.groupable = false
149 149 @association = association
150 150 end
151 151
152 152 def value_object(object)
153 153 if assoc = object.send(@association)
154 154 super(assoc)
155 155 end
156 156 end
157 157
158 158 def css_classes
159 159 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
160 160 end
161 161 end
162 162
163 class QueryFilter
164 include Redmine::I18n
165
166 def initialize(field, options)
167 @field = field.to_s
168 @options = options
169 @options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
170 # Consider filters with a Proc for values as remote by default
171 @remote = options.key?(:remote) ? options[:remote] : options[:values].is_a?(Proc)
172 end
173
174 def [](arg)
175 if arg == :values
176 values
177 else
178 @options[arg]
179 end
180 end
181
182 def values
183 @values ||= begin
184 values = @options[:values]
185 if values.is_a?(Proc)
186 values = values.call
187 end
188 values
189 end
190 end
191
192 def remote
193 @remote
194 end
195 end
196
163 197 class Query < ActiveRecord::Base
164 198 class StatementInvalid < ::ActiveRecord::StatementInvalid
165 199 end
166 200
167 201 include Redmine::SubclassFactory
168 202
169 203 VISIBILITY_PRIVATE = 0
170 204 VISIBILITY_ROLES = 1
171 205 VISIBILITY_PUBLIC = 2
172 206
173 207 belongs_to :project
174 208 belongs_to :user
175 209 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
176 210 serialize :filters
177 211 serialize :column_names
178 212 serialize :sort_criteria, Array
179 213 serialize :options, Hash
180 214
181 215 attr_protected :project_id, :user_id
182 216
183 217 validates_presence_of :name
184 218 validates_length_of :name, :maximum => 255
185 219 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
186 220 validate :validate_query_filters
187 221 validate do |query|
188 222 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
189 223 end
190 224
191 225 after_save do |query|
192 226 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
193 227 query.roles.clear
194 228 end
195 229 end
196 230
197 231 class_attribute :operators
198 232 self.operators = {
199 233 "=" => :label_equals,
200 234 "!" => :label_not_equals,
201 235 "o" => :label_open_issues,
202 236 "c" => :label_closed_issues,
203 237 "!*" => :label_none,
204 238 "*" => :label_any,
205 239 ">=" => :label_greater_or_equal,
206 240 "<=" => :label_less_or_equal,
207 241 "><" => :label_between,
208 242 "<t+" => :label_in_less_than,
209 243 ">t+" => :label_in_more_than,
210 244 "><t+"=> :label_in_the_next_days,
211 245 "t+" => :label_in,
212 246 "t" => :label_today,
213 247 "ld" => :label_yesterday,
214 248 "w" => :label_this_week,
215 249 "lw" => :label_last_week,
216 250 "l2w" => [:label_last_n_weeks, {:count => 2}],
217 251 "m" => :label_this_month,
218 252 "lm" => :label_last_month,
219 253 "y" => :label_this_year,
220 254 ">t-" => :label_less_than_ago,
221 255 "<t-" => :label_more_than_ago,
222 256 "><t-"=> :label_in_the_past_days,
223 257 "t-" => :label_ago,
224 258 "~" => :label_contains,
225 259 "!~" => :label_not_contains,
226 260 "=p" => :label_any_issues_in_project,
227 261 "=!p" => :label_any_issues_not_in_project,
228 262 "!p" => :label_no_issues_in_project,
229 263 "*o" => :label_any_open_issues,
230 264 "!o" => :label_no_open_issues
231 265 }
232 266
233 267 class_attribute :operators_by_filter_type
234 268 self.operators_by_filter_type = {
235 269 :list => [ "=", "!" ],
236 270 :list_status => [ "o", "=", "!", "c", "*" ],
237 271 :list_optional => [ "=", "!", "!*", "*" ],
238 272 :list_subprojects => [ "*", "!*", "=" ],
239 273 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
240 274 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
241 275 :string => [ "=", "~", "!", "!~", "!*", "*" ],
242 276 :text => [ "~", "!~", "!*", "*" ],
243 277 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
244 278 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
245 279 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
246 280 :tree => ["=", "~", "!*", "*"]
247 281 }
248 282
249 283 class_attribute :available_columns
250 284 self.available_columns = []
251 285
252 286 class_attribute :queried_class
253 287
254 288 # Permission required to view the queries, set on subclasses.
255 289 class_attribute :view_permission
256 290
257 291 # Scope of queries that are global or on the given project
258 292 scope :global_or_on_project, lambda {|project|
259 293 where(:project_id => (project.nil? ? nil : [nil, project.id]))
260 294 }
261 295
262 296 scope :sorted, lambda {order(:name, :id)}
263 297
264 298 # Scope of visible queries, can be used from subclasses only.
265 299 # Unlike other visible scopes, a class methods is used as it
266 300 # let handle inheritance more nicely than scope DSL.
267 301 def self.visible(*args)
268 302 if self == ::Query
269 303 # Visibility depends on permissions for each subclass,
270 304 # raise an error if the scope is called from Query (eg. Query.visible)
271 305 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
272 306 end
273 307
274 308 user = args.shift || User.current
275 309 base = Project.allowed_to_condition(user, view_permission, *args)
276 310 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
277 311 where("#{table_name}.project_id IS NULL OR (#{base})")
278 312
279 313 if user.admin?
280 314 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
281 315 elsif user.memberships.any?
282 316 scope.where("#{table_name}.visibility = ?" +
283 317 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
284 318 "SELECT DISTINCT q.id FROM #{table_name} q" +
285 319 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
286 320 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
287 321 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
288 322 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
289 323 " OR #{table_name}.user_id = ?",
290 324 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
291 325 elsif user.logged?
292 326 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
293 327 else
294 328 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
295 329 end
296 330 end
297 331
298 332 # Returns true if the query is visible to +user+ or the current user.
299 333 def visible?(user=User.current)
300 334 return true if user.admin?
301 335 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
302 336 case visibility
303 337 when VISIBILITY_PUBLIC
304 338 true
305 339 when VISIBILITY_ROLES
306 340 if project
307 341 (user.roles_for_project(project) & roles).any?
308 342 else
309 343 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
310 344 end
311 345 else
312 346 user == self.user
313 347 end
314 348 end
315 349
316 350 def is_private?
317 351 visibility == VISIBILITY_PRIVATE
318 352 end
319 353
320 354 def is_public?
321 355 !is_private?
322 356 end
323 357
324 358 def queried_table_name
325 359 @queried_table_name ||= self.class.queried_class.table_name
326 360 end
327 361
328 362 def initialize(attributes=nil, *args)
329 363 super attributes
330 364 @is_for_all = project.nil?
331 365 end
332 366
333 367 # Builds the query from the given params
334 368 def build_from_params(params)
335 369 if params[:fields] || params[:f]
336 370 self.filters = {}
337 371 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
338 372 else
339 373 available_filters.keys.each do |field|
340 374 add_short_filter(field, params[field]) if params[field]
341 375 end
342 376 end
343 377 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
344 378 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
345 379 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
346 380 self
347 381 end
348 382
349 383 # Builds a new query from the given params and attributes
350 384 def self.build_from_params(params, attributes={})
351 385 new(attributes).build_from_params(params)
352 386 end
353 387
354 388 def validate_query_filters
355 389 filters.each_key do |field|
356 390 if values_for(field)
357 391 case type_for(field)
358 392 when :integer
359 393 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
360 394 when :float
361 395 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
362 396 when :date, :date_past
363 397 case operator_for(field)
364 398 when "=", ">=", "<=", "><"
365 399 add_filter_error(field, :invalid) if values_for(field).detect {|v|
366 400 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
367 401 }
368 402 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
369 403 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
370 404 end
371 405 end
372 406 end
373 407
374 408 add_filter_error(field, :blank) unless
375 409 # filter requires one or more values
376 410 (values_for(field) and !values_for(field).first.blank?) or
377 411 # filter doesn't require any value
378 412 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
379 413 end if filters
380 414 end
381 415
382 416 def add_filter_error(field, message)
383 417 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
384 418 errors.add(:base, m)
385 419 end
386 420
387 421 def editable_by?(user)
388 422 return false unless user
389 423 # Admin can edit them all and regular users can edit their private queries
390 424 return true if user.admin? || (is_private? && self.user_id == user.id)
391 425 # Members can not edit public queries that are for all project (only admin is allowed to)
392 426 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
393 427 end
394 428
395 429 def trackers
396 430 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
397 431 end
398 432
399 433 # Returns a hash of localized labels for all filter operators
400 434 def self.operators_labels
401 435 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
402 436 end
403 437
404 438 # Returns a representation of the available filters for JSON serialization
405 439 def available_filters_as_json
406 440 json = {}
407 available_filters.each do |field, options|
408 options = options.slice(:type, :name, :values)
409 if options[:values] && values_for(field)
410 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
411 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
412 options[:values] += send(method, missing)
441 available_filters.each do |field, filter|
442 options = {:type => filter[:type], :name => filter[:name]}
443 options[:remote] = true if filter.remote
444
445 if has_filter?(field) || !filter.remote
446 options[:values] = filter.values
447 if options[:values] && values_for(field)
448 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
449 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
450 options[:values] += send(method, missing)
451 end
413 452 end
414 453 end
415 454 json[field] = options.stringify_keys
416 455 end
417 456 json
418 457 end
419 458
420 459 def all_projects
421 460 @all_projects ||= Project.visible.to_a
422 461 end
423 462
424 463 def all_projects_values
425 464 return @all_projects_values if @all_projects_values
426 465
427 466 values = []
428 467 Project.project_tree(all_projects) do |p, level|
429 468 prefix = (level > 0 ? ('--' * level + ' ') : '')
430 469 values << ["#{prefix}#{p.name}", p.id.to_s]
431 470 end
432 471 @all_projects_values = values
433 472 end
434 473
474 def project_values
475 project_values = []
476 if User.current.logged? && User.current.memberships.any?
477 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
478 end
479 project_values += all_projects_values
480 project_values
481 end
482
483 def subproject_values
484 project.descendants.visible.collect{|s| [s.name, s.id.to_s] }
485 end
486
487 def principals
488 @principal ||= begin
489 principals = []
490 if project
491 principals += project.principals.visible
492 unless project.leaf?
493 principals += Principal.member_of(project.descendants.visible).visible
494 end
495 else
496 principals += Principal.member_of(all_projects).visible
497 end
498 principals.uniq!
499 principals.sort!
500 principals.reject! {|p| p.is_a?(GroupBuiltin)}
501 principals
502 end
503 end
504
505 def users
506 principals.select {|p| p.is_a?(User)}
507 end
508
509 def author_values
510 author_values = []
511 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
512 author_values += users.collect{|s| [s.name, s.id.to_s] }
513 author_values
514 end
515
516 def assigned_to_values
517 assigned_to_values = []
518 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
519 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
520 assigned_to_values
521 end
522
523 def fixed_version_values
524 versions = []
525 if project
526 versions = project.shared_versions.to_a
527 else
528 versions = Version.visible.where(:sharing => 'system').to_a
529 end
530 Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] }
531 end
532
435 533 # Adds available filters
436 534 def initialize_available_filters
437 535 # implemented by sub-classes
438 536 end
439 537 protected :initialize_available_filters
440 538
441 539 # Adds an available filter
442 540 def add_available_filter(field, options)
443 541 @available_filters ||= ActiveSupport::OrderedHash.new
444 @available_filters[field] = options
542 @available_filters[field] = QueryFilter.new(field, options)
445 543 @available_filters
446 544 end
447 545
448 546 # Removes an available filter
449 547 def delete_available_filter(field)
450 548 if @available_filters
451 549 @available_filters.delete(field)
452 550 end
453 551 end
454 552
455 553 # Return a hash of available filters
456 554 def available_filters
457 555 unless @available_filters
458 556 initialize_available_filters
459 557 @available_filters ||= {}
460 @available_filters.each do |field, options|
461 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
462 end
463 558 end
464 559 @available_filters
465 560 end
466 561
467 562 def add_filter(field, operator, values=nil)
468 563 # values must be an array
469 564 return unless values.nil? || values.is_a?(Array)
470 565 # check if field is defined as an available filter
471 566 if available_filters.has_key? field
472 567 filter_options = available_filters[field]
473 568 filters[field] = {:operator => operator, :values => (values || [''])}
474 569 end
475 570 end
476 571
477 572 def add_short_filter(field, expression)
478 573 return unless expression && available_filters.has_key?(field)
479 574 field_type = available_filters[field][:type]
480 575 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
481 576 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
482 577 values = $1
483 578 add_filter field, operator, values.present? ? values.split('|') : ['']
484 579 end || add_filter(field, '=', expression.to_s.split('|'))
485 580 end
486 581
487 582 # Add multiple filters using +add_filter+
488 583 def add_filters(fields, operators, values)
489 584 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
490 585 fields.each do |field|
491 586 add_filter(field, operators[field], values && values[field])
492 587 end
493 588 end
494 589 end
495 590
496 591 def has_filter?(field)
497 592 filters and filters[field]
498 593 end
499 594
500 595 def type_for(field)
501 596 available_filters[field][:type] if available_filters.has_key?(field)
502 597 end
503 598
504 599 def operator_for(field)
505 600 has_filter?(field) ? filters[field][:operator] : nil
506 601 end
507 602
508 603 def values_for(field)
509 604 has_filter?(field) ? filters[field][:values] : nil
510 605 end
511 606
512 607 def value_for(field, index=0)
513 608 (values_for(field) || [])[index]
514 609 end
515 610
516 611 def label_for(field)
517 612 label = available_filters[field][:name] if available_filters.has_key?(field)
518 613 label ||= queried_class.human_attribute_name(field, :default => field)
519 614 end
520 615
521 616 def self.add_available_column(column)
522 617 self.available_columns << (column) if column.is_a?(QueryColumn)
523 618 end
524 619
525 620 # Returns an array of columns that can be used to group the results
526 621 def groupable_columns
527 622 available_columns.select {|c| c.groupable}
528 623 end
529 624
530 625 # Returns a Hash of columns and the key for sorting
531 626 def sortable_columns
532 627 available_columns.inject({}) {|h, column|
533 628 h[column.name.to_s] = column.sortable
534 629 h
535 630 }
536 631 end
537 632
538 633 def columns
539 634 # preserve the column_names order
540 635 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
541 636 available_columns.find { |col| col.name == name }
542 637 end.compact
543 638 available_columns.select(&:frozen?) | cols
544 639 end
545 640
546 641 def inline_columns
547 642 columns.select(&:inline?)
548 643 end
549 644
550 645 def block_columns
551 646 columns.reject(&:inline?)
552 647 end
553 648
554 649 def available_inline_columns
555 650 available_columns.select(&:inline?)
556 651 end
557 652
558 653 def available_block_columns
559 654 available_columns.reject(&:inline?)
560 655 end
561 656
562 657 def available_totalable_columns
563 658 available_columns.select(&:totalable)
564 659 end
565 660
566 661 def default_columns_names
567 662 []
568 663 end
569 664
570 665 def default_totalable_names
571 666 []
572 667 end
573 668
574 669 def column_names=(names)
575 670 if names
576 671 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
577 672 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
578 673 # Set column_names to nil if default columns
579 674 if names == default_columns_names
580 675 names = nil
581 676 end
582 677 end
583 678 write_attribute(:column_names, names)
584 679 end
585 680
586 681 def has_column?(column)
587 682 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
588 683 end
589 684
590 685 def has_custom_field_column?
591 686 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
592 687 end
593 688
594 689 def has_default_columns?
595 690 column_names.nil? || column_names.empty?
596 691 end
597 692
598 693 def totalable_columns
599 694 names = totalable_names
600 695 available_totalable_columns.select {|column| names.include?(column.name)}
601 696 end
602 697
603 698 def totalable_names=(names)
604 699 if names
605 700 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
606 701 end
607 702 options[:totalable_names] = names
608 703 end
609 704
610 705 def totalable_names
611 706 options[:totalable_names] || default_totalable_names || []
612 707 end
613 708
614 709 def sort_criteria=(arg)
615 710 c = []
616 711 if arg.is_a?(Hash)
617 712 arg = arg.keys.sort.collect {|k| arg[k]}
618 713 end
619 714 if arg
620 715 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
621 716 end
622 717 write_attribute(:sort_criteria, c)
623 718 end
624 719
625 720 def sort_criteria
626 721 read_attribute(:sort_criteria) || []
627 722 end
628 723
629 724 def sort_criteria_key(arg)
630 725 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
631 726 end
632 727
633 728 def sort_criteria_order(arg)
634 729 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
635 730 end
636 731
637 732 def sort_criteria_order_for(key)
638 733 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
639 734 end
640 735
641 736 # Returns the SQL sort order that should be prepended for grouping
642 737 def group_by_sort_order
643 738 if column = group_by_column
644 739 order = (sort_criteria_order_for(column.name) || column.default_order || 'asc').try(:upcase)
645 740 Array(column.sortable).map {|s| "#{s} #{order}"}
646 741 end
647 742 end
648 743
649 744 # Returns true if the query is a grouped query
650 745 def grouped?
651 746 !group_by_column.nil?
652 747 end
653 748
654 749 def group_by_column
655 750 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
656 751 end
657 752
658 753 def group_by_statement
659 754 group_by_column.try(:groupable)
660 755 end
661 756
662 757 def project_statement
663 758 project_clauses = []
664 759 if project && !project.descendants.active.empty?
665 760 if has_filter?("subproject_id")
666 761 case operator_for("subproject_id")
667 762 when '='
668 763 # include the selected subprojects
669 764 ids = [project.id] + values_for("subproject_id").each(&:to_i)
670 765 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
671 766 when '!*'
672 767 # main project only
673 768 project_clauses << "#{Project.table_name}.id = %d" % project.id
674 769 else
675 770 # all subprojects
676 771 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
677 772 end
678 773 elsif Setting.display_subprojects_issues?
679 774 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
680 775 else
681 776 project_clauses << "#{Project.table_name}.id = %d" % project.id
682 777 end
683 778 elsif project
684 779 project_clauses << "#{Project.table_name}.id = %d" % project.id
685 780 end
686 781 project_clauses.any? ? project_clauses.join(' AND ') : nil
687 782 end
688 783
689 784 def statement
690 785 # filters clauses
691 786 filters_clauses = []
692 787 filters.each_key do |field|
693 788 next if field == "subproject_id"
694 789 v = values_for(field).clone
695 790 next unless v and !v.empty?
696 791 operator = operator_for(field)
697 792
698 793 # "me" value substitution
699 794 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
700 795 if v.delete("me")
701 796 if User.current.logged?
702 797 v.push(User.current.id.to_s)
703 798 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
704 799 else
705 800 v.push("0")
706 801 end
707 802 end
708 803 end
709 804
710 805 if field == 'project_id'
711 806 if v.delete('mine')
712 807 v += User.current.memberships.map(&:project_id).map(&:to_s)
713 808 end
714 809 end
715 810
716 811 if field =~ /cf_(\d+)$/
717 812 # custom field
718 813 filters_clauses << sql_for_custom_field(field, operator, v, $1)
719 814 elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
720 815 # specific statement
721 816 filters_clauses << send(method, field, operator, v)
722 817 else
723 818 # regular field
724 819 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
725 820 end
726 821 end if filters and valid?
727 822
728 823 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
729 824 # Excludes results for which the grouped custom field is not visible
730 825 filters_clauses << c.custom_field.visibility_by_project_condition
731 826 end
732 827
733 828 filters_clauses << project_statement
734 829 filters_clauses.reject!(&:blank?)
735 830
736 831 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
737 832 end
738 833
739 834 # Returns the sum of values for the given column
740 835 def total_for(column)
741 836 total_with_scope(column, base_scope)
742 837 end
743 838
744 839 # Returns a hash of the sum of the given column for each group,
745 840 # or nil if the query is not grouped
746 841 def total_by_group_for(column)
747 842 grouped_query do |scope|
748 843 total_with_scope(column, scope)
749 844 end
750 845 end
751 846
752 847 def totals
753 848 totals = totalable_columns.map {|column| [column, total_for(column)]}
754 849 yield totals if block_given?
755 850 totals
756 851 end
757 852
758 853 def totals_by_group
759 854 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
760 855 yield totals if block_given?
761 856 totals
762 857 end
763 858
764 859 private
765 860
766 861 def grouped_query(&block)
767 862 r = nil
768 863 if grouped?
769 864 begin
770 865 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
771 866 r = yield base_group_scope
772 867 rescue ActiveRecord::RecordNotFound
773 868 r = {nil => yield(base_scope)}
774 869 end
775 870 c = group_by_column
776 871 if c.is_a?(QueryCustomFieldColumn)
777 872 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
778 873 end
779 874 end
780 875 r
781 876 rescue ::ActiveRecord::StatementInvalid => e
782 877 raise StatementInvalid.new(e.message)
783 878 end
784 879
785 880 def total_with_scope(column, scope)
786 881 unless column.is_a?(QueryColumn)
787 882 column = column.to_sym
788 883 column = available_totalable_columns.detect {|c| c.name == column}
789 884 end
790 885 if column.is_a?(QueryCustomFieldColumn)
791 886 custom_field = column.custom_field
792 887 send "total_for_custom_field", custom_field, scope
793 888 else
794 889 send "total_for_#{column.name}", scope
795 890 end
796 891 rescue ::ActiveRecord::StatementInvalid => e
797 892 raise StatementInvalid.new(e.message)
798 893 end
799 894
800 895 def base_scope
801 896 raise "unimplemented"
802 897 end
803 898
804 899 def base_group_scope
805 900 base_scope.
806 901 joins(joins_for_order_statement(group_by_statement)).
807 902 group(group_by_statement)
808 903 end
809 904
810 905 def total_for_custom_field(custom_field, scope, &block)
811 906 total = custom_field.format.total_for_scope(custom_field, scope)
812 907 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
813 908 total
814 909 end
815 910
816 911 def map_total(total, &block)
817 912 if total.is_a?(Hash)
818 913 total.keys.each {|k| total[k] = yield total[k]}
819 914 else
820 915 total = yield total
821 916 end
822 917 total
823 918 end
824 919
825 920 def sql_for_custom_field(field, operator, value, custom_field_id)
826 921 db_table = CustomValue.table_name
827 922 db_field = 'value'
828 923 filter = @available_filters[field]
829 924 return nil unless filter
830 925 if filter[:field].format.target_class && filter[:field].format.target_class <= User
831 926 if value.delete('me')
832 927 value.push User.current.id.to_s
833 928 end
834 929 end
835 930 not_in = nil
836 931 if operator == '!'
837 932 # Makes ! operator work for custom fields with multiple values
838 933 operator = '='
839 934 not_in = 'NOT'
840 935 end
841 936 customized_key = "id"
842 937 customized_class = queried_class
843 938 if field =~ /^(.+)\.cf_/
844 939 assoc = $1
845 940 customized_key = "#{assoc}_id"
846 941 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
847 942 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
848 943 end
849 944 where = sql_for_field(field, operator, value, db_table, db_field, true)
850 945 if operator =~ /[<>]/
851 946 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
852 947 end
853 948 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
854 949 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
855 950 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
856 951 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
857 952 end
858 953
859 954 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
860 955 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
861 956 sql = ''
862 957 case operator
863 958 when "="
864 959 if value.any?
865 960 case type_for(field)
866 961 when :date, :date_past
867 962 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
868 963 when :integer
869 964 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
870 965 if int_values.present?
871 966 if is_custom_filter
872 967 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) IN (#{int_values}))"
873 968 else
874 969 sql = "#{db_table}.#{db_field} IN (#{int_values})"
875 970 end
876 971 else
877 972 sql = "1=0"
878 973 end
879 974 when :float
880 975 if is_custom_filter
881 976 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
882 977 else
883 978 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
884 979 end
885 980 else
886 981 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
887 982 end
888 983 else
889 984 # IN an empty set
890 985 sql = "1=0"
891 986 end
892 987 when "!"
893 988 if value.any?
894 989 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
895 990 else
896 991 # NOT IN an empty set
897 992 sql = "1=1"
898 993 end
899 994 when "!*"
900 995 sql = "#{db_table}.#{db_field} IS NULL"
901 996 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
902 997 when "*"
903 998 sql = "#{db_table}.#{db_field} IS NOT NULL"
904 999 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
905 1000 when ">="
906 1001 if [:date, :date_past].include?(type_for(field))
907 1002 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
908 1003 else
909 1004 if is_custom_filter
910 1005 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
911 1006 else
912 1007 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
913 1008 end
914 1009 end
915 1010 when "<="
916 1011 if [:date, :date_past].include?(type_for(field))
917 1012 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
918 1013 else
919 1014 if is_custom_filter
920 1015 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
921 1016 else
922 1017 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
923 1018 end
924 1019 end
925 1020 when "><"
926 1021 if [:date, :date_past].include?(type_for(field))
927 1022 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
928 1023 else
929 1024 if is_custom_filter
930 1025 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
931 1026 else
932 1027 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
933 1028 end
934 1029 end
935 1030 when "o"
936 1031 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
937 1032 when "c"
938 1033 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
939 1034 when "><t-"
940 1035 # between today - n days and today
941 1036 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
942 1037 when ">t-"
943 1038 # >= today - n days
944 1039 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
945 1040 when "<t-"
946 1041 # <= today - n days
947 1042 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
948 1043 when "t-"
949 1044 # = n days in past
950 1045 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
951 1046 when "><t+"
952 1047 # between today and today + n days
953 1048 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
954 1049 when ">t+"
955 1050 # >= today + n days
956 1051 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
957 1052 when "<t+"
958 1053 # <= today + n days
959 1054 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
960 1055 when "t+"
961 1056 # = today + n days
962 1057 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
963 1058 when "t"
964 1059 # = today
965 1060 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
966 1061 when "ld"
967 1062 # = yesterday
968 1063 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
969 1064 when "w"
970 1065 # = this week
971 1066 first_day_of_week = l(:general_first_day_of_week).to_i
972 1067 day_of_week = User.current.today.cwday
973 1068 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
974 1069 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
975 1070 when "lw"
976 1071 # = last week
977 1072 first_day_of_week = l(:general_first_day_of_week).to_i
978 1073 day_of_week = User.current.today.cwday
979 1074 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
980 1075 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
981 1076 when "l2w"
982 1077 # = last 2 weeks
983 1078 first_day_of_week = l(:general_first_day_of_week).to_i
984 1079 day_of_week = User.current.today.cwday
985 1080 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
986 1081 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
987 1082 when "m"
988 1083 # = this month
989 1084 date = User.current.today
990 1085 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
991 1086 when "lm"
992 1087 # = last month
993 1088 date = User.current.today.prev_month
994 1089 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
995 1090 when "y"
996 1091 # = this year
997 1092 date = User.current.today
998 1093 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
999 1094 when "~"
1000 1095 sql = sql_contains("#{db_table}.#{db_field}", value.first)
1001 1096 when "!~"
1002 1097 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
1003 1098 else
1004 1099 raise "Unknown query operator #{operator}"
1005 1100 end
1006 1101
1007 1102 return sql
1008 1103 end
1009 1104
1010 1105 # Returns a SQL LIKE statement with wildcards
1011 1106 def sql_contains(db_field, value, match=true)
1012 1107 queried_class.send :sanitize_sql_for_conditions,
1013 1108 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
1014 1109 end
1015 1110
1016 1111 # Adds a filter for the given custom field
1017 1112 def add_custom_field_filter(field, assoc=nil)
1018 1113 options = field.query_filter_options(self)
1019 1114 if field.format.target_class && field.format.target_class <= User
1020 1115 if options[:values].is_a?(Array) && User.current.logged?
1021 1116 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
1022 1117 end
1023 1118 end
1024 1119
1025 1120 filter_id = "cf_#{field.id}"
1026 1121 filter_name = field.name
1027 1122 if assoc.present?
1028 1123 filter_id = "#{assoc}.#{filter_id}"
1029 1124 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1030 1125 end
1031 1126 add_available_filter filter_id, options.merge({
1032 1127 :name => filter_name,
1033 1128 :field => field
1034 1129 })
1035 1130 end
1036 1131
1037 1132 # Adds filters for the given custom fields scope
1038 1133 def add_custom_fields_filters(scope, assoc=nil)
1039 1134 scope.visible.where(:is_filter => true).sorted.each do |field|
1040 1135 add_custom_field_filter(field, assoc)
1041 1136 end
1042 1137 end
1043 1138
1044 1139 # Adds filters for the given associations custom fields
1045 1140 def add_associations_custom_fields_filters(*associations)
1046 1141 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1047 1142 associations.each do |assoc|
1048 1143 association_klass = queried_class.reflect_on_association(assoc).klass
1049 1144 fields_by_class.each do |field_class, fields|
1050 1145 if field_class.customized_class <= association_klass
1051 1146 fields.sort.each do |field|
1052 1147 add_custom_field_filter(field, assoc)
1053 1148 end
1054 1149 end
1055 1150 end
1056 1151 end
1057 1152 end
1058 1153
1059 1154 def quoted_time(time, is_custom_filter)
1060 1155 if is_custom_filter
1061 1156 # Custom field values are stored as strings in the DB
1062 1157 # using this format that does not depend on DB date representation
1063 1158 time.strftime("%Y-%m-%d %H:%M:%S")
1064 1159 else
1065 1160 self.class.connection.quoted_date(time)
1066 1161 end
1067 1162 end
1068 1163
1069 1164 def date_for_user_time_zone(y, m, d)
1070 1165 if tz = User.current.time_zone
1071 1166 tz.local y, m, d
1072 1167 else
1073 1168 Time.local y, m, d
1074 1169 end
1075 1170 end
1076 1171
1077 1172 # Returns a SQL clause for a date or datetime field.
1078 1173 def date_clause(table, field, from, to, is_custom_filter)
1079 1174 s = []
1080 1175 if from
1081 1176 if from.is_a?(Date)
1082 1177 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1083 1178 else
1084 1179 from = from - 1 # second
1085 1180 end
1086 1181 if self.class.default_timezone == :utc
1087 1182 from = from.utc
1088 1183 end
1089 1184 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1090 1185 end
1091 1186 if to
1092 1187 if to.is_a?(Date)
1093 1188 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1094 1189 end
1095 1190 if self.class.default_timezone == :utc
1096 1191 to = to.utc
1097 1192 end
1098 1193 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1099 1194 end
1100 1195 s.join(' AND ')
1101 1196 end
1102 1197
1103 1198 # Returns a SQL clause for a date or datetime field using relative dates.
1104 1199 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1105 1200 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1106 1201 end
1107 1202
1108 1203 # Returns a Date or Time from the given filter value
1109 1204 def parse_date(arg)
1110 1205 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1111 1206 Time.parse(arg) rescue nil
1112 1207 else
1113 1208 Date.parse(arg) rescue nil
1114 1209 end
1115 1210 end
1116 1211
1117 1212 # Additional joins required for the given sort options
1118 1213 def joins_for_order_statement(order_options)
1119 1214 joins = []
1120 1215
1121 1216 if order_options
1122 1217 if order_options.include?('authors')
1123 1218 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1124 1219 end
1125 1220 order_options.scan(/cf_\d+/).uniq.each do |name|
1126 1221 column = available_columns.detect {|c| c.name.to_s == name}
1127 1222 join = column && column.custom_field.join_for_order_statement
1128 1223 if join
1129 1224 joins << join
1130 1225 end
1131 1226 end
1132 1227 end
1133 1228
1134 1229 joins.any? ? joins.join(' ') : nil
1135 1230 end
1136 1231 end
@@ -1,250 +1,223
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TimeEntryQuery < Query
19 19
20 20 self.queried_class = TimeEntry
21 21 self.view_permission = :view_time_entries
22 22
23 23 self.available_columns = [
24 24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25 25 QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
26 26 QueryColumn.new(:tweek, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :caption => l(:label_week)),
27 27 QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
28 28 QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
29 29 QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
30 30 QueryAssociationColumn.new(:issue, :tracker, :caption => :field_tracker, :sortable => "#{Tracker.table_name}.position"),
31 31 QueryAssociationColumn.new(:issue, :status, :caption => :field_status, :sortable => "#{IssueStatus.table_name}.position"),
32 32 QueryColumn.new(:comments),
33 33 QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours", :totalable => true),
34 34 ]
35 35
36 36 def initialize(attributes=nil, *args)
37 37 super attributes
38 38 self.filters ||= {}
39 39 add_filter('spent_on', '*') unless filters.present?
40 40 end
41 41
42 42 def initialize_available_filters
43 43 add_available_filter "spent_on", :type => :date_past
44 44
45 principals = []
46 versions = []
47 if project
48 principals += project.principals.visible.sort
49 unless project.leaf?
50 subprojects = project.descendants.visible.to_a
51 if subprojects.any?
52 add_available_filter "subproject_id",
53 :type => :list_subprojects,
54 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
55 principals += Principal.member_of(subprojects).visible
56 end
57 end
58 versions = project.shared_versions.to_a
59 else
60 if all_projects.any?
61 # members of visible projects
62 principals += Principal.member_of(all_projects).visible
63 # project filter
64 project_values = []
65 if User.current.logged? && User.current.memberships.any?
66 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
67 end
68 project_values += all_projects_values
69 add_available_filter("project_id",
70 :type => :list, :values => project_values
71 ) unless project_values.empty?
72 end
45 add_available_filter("project_id",
46 :type => :list, :values => lambda { project_values }
47 ) if project.nil?
48
49 if project && !project.leaf?
50 add_available_filter "subproject_id",
51 :type => :list_subprojects,
52 :values => lambda { subproject_values }
73 53 end
74 54
75 55 add_available_filter("issue_id", :type => :tree, :label => :label_issue)
76 56 add_available_filter("issue.tracker_id",
77 57 :type => :list,
78 58 :name => l("label_attribute_of_issue", :name => l(:field_tracker)),
79 :values => Tracker.sorted.map {|t| [t.name, t.id.to_s]})
59 :values => lambda { Tracker.sorted.map {|t| [t.name, t.id.to_s]} })
80 60 add_available_filter("issue.status_id",
81 61 :type => :list,
82 62 :name => l("label_attribute_of_issue", :name => l(:field_status)),
83 :values => IssueStatus.sorted.map {|s| [s.name, s.id.to_s]})
63 :values => lambda { IssueStatus.sorted.map {|s| [s.name, s.id.to_s]} })
84 64 add_available_filter("issue.fixed_version_id",
85 65 :type => :list,
86 66 :name => l("label_attribute_of_issue", :name => l(:field_fixed_version)),
87 :values => Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] })
88
89 principals.uniq!
90 principals.sort!
91 users = principals.select {|p| p.is_a?(User)}
67 :values => lambda { fixed_version_values }) if project
92 68
93 users_values = []
94 users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
95 users_values += users.collect{|s| [s.name, s.id.to_s] }
96 69 add_available_filter("user_id",
97 :type => :list_optional, :values => users_values
98 ) unless users_values.empty?
70 :type => :list_optional, :values => lambda { author_values }
71 )
99 72
100 73 activities = (project ? project.activities : TimeEntryActivity.shared)
101 74 add_available_filter("activity_id",
102 75 :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
103 ) unless activities.empty?
76 )
104 77
105 78 add_available_filter "comments", :type => :text
106 79 add_available_filter "hours", :type => :float
107 80
108 81 add_custom_fields_filters(TimeEntryCustomField)
109 82 add_associations_custom_fields_filters :project
110 83 add_custom_fields_filters(issue_custom_fields, :issue)
111 84 add_associations_custom_fields_filters :user
112 85 end
113 86
114 87 def available_columns
115 88 return @available_columns if @available_columns
116 89 @available_columns = self.class.available_columns.dup
117 90 @available_columns += TimeEntryCustomField.visible.
118 91 map {|cf| QueryCustomFieldColumn.new(cf) }
119 92 @available_columns += issue_custom_fields.visible.
120 93 map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false) }
121 94 @available_columns
122 95 end
123 96
124 97 def default_columns_names
125 98 @default_columns_names ||= begin
126 99 default_columns = [:spent_on, :user, :activity, :issue, :comments, :hours]
127 100
128 101 project.present? ? default_columns : [:project] | default_columns
129 102 end
130 103 end
131 104
132 105 def default_totalable_names
133 106 [:hours]
134 107 end
135 108
136 109 def base_scope
137 110 TimeEntry.visible.
138 111 joins(:project, :user).
139 112 joins("LEFT OUTER JOIN issues ON issues.id = time_entries.issue_id").
140 113 where(statement)
141 114 end
142 115
143 116 def results_scope(options={})
144 117 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
145 118
146 119 base_scope.
147 120 order(order_option).
148 121 joins(joins_for_order_statement(order_option.join(','))).
149 122 includes(:activity).
150 123 references(:activity)
151 124 end
152 125
153 126 # Returns sum of all the spent hours
154 127 def total_for_hours(scope)
155 128 map_total(scope.sum(:hours)) {|t| t.to_f.round(2)}
156 129 end
157 130
158 131 def sql_for_issue_id_field(field, operator, value)
159 132 case operator
160 133 when "="
161 134 "#{TimeEntry.table_name}.issue_id = #{value.first.to_i}"
162 135 when "~"
163 136 issue = Issue.where(:id => value.first.to_i).first
164 137 if issue && (issue_ids = issue.self_and_descendants.pluck(:id)).any?
165 138 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
166 139 else
167 140 "1=0"
168 141 end
169 142 when "!*"
170 143 "#{TimeEntry.table_name}.issue_id IS NULL"
171 144 when "*"
172 145 "#{TimeEntry.table_name}.issue_id IS NOT NULL"
173 146 end
174 147 end
175 148
176 149 def sql_for_issue_fixed_version_id_field(field, operator, value)
177 150 issue_ids = Issue.where(:fixed_version_id => value.first.to_i).pluck(:id)
178 151 case operator
179 152 when "="
180 153 if issue_ids.any?
181 154 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
182 155 else
183 156 "1=0"
184 157 end
185 158 when "!"
186 159 if issue_ids.any?
187 160 "#{TimeEntry.table_name}.issue_id NOT IN (#{issue_ids.join(',')})"
188 161 else
189 162 "1=1"
190 163 end
191 164 end
192 165 end
193 166
194 167 def sql_for_activity_id_field(field, operator, value)
195 168 condition_on_id = sql_for_field(field, operator, value, Enumeration.table_name, 'id')
196 169 condition_on_parent_id = sql_for_field(field, operator, value, Enumeration.table_name, 'parent_id')
197 170 ids = value.map(&:to_i).join(',')
198 171 table_name = Enumeration.table_name
199 172 if operator == '='
200 173 "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))"
201 174 else
202 175 "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))"
203 176 end
204 177 end
205 178
206 179 def sql_for_issue_tracker_id_field(field, operator, value)
207 180 sql_for_field("tracker_id", operator, value, Issue.table_name, "tracker_id")
208 181 end
209 182
210 183 def sql_for_issue_status_id_field(field, operator, value)
211 184 sql_for_field("status_id", operator, value, Issue.table_name, "status_id")
212 185 end
213 186
214 187 # Accepts :from/:to params as shortcut filters
215 188 def build_from_params(params)
216 189 super
217 190 if params[:from].present? && params[:to].present?
218 191 add_filter('spent_on', '><', [params[:from], params[:to]])
219 192 elsif params[:from].present?
220 193 add_filter('spent_on', '>=', [params[:from]])
221 194 elsif params[:to].present?
222 195 add_filter('spent_on', '<=', [params[:to]])
223 196 end
224 197 self
225 198 end
226 199
227 200 def joins_for_order_statement(order_options)
228 201 joins = [super]
229 202
230 203 if order_options
231 204 if order_options.include?('issue_statuses')
232 205 joins << "LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id"
233 206 end
234 207 if order_options.include?('trackers')
235 208 joins << "LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id"
236 209 end
237 210 end
238 211
239 212 joins.compact!
240 213 joins.any? ? joins.join(' ') : nil
241 214 end
242 215
243 216 def issue_custom_fields
244 217 if project
245 218 project.all_issue_custom_fields
246 219 else
247 220 IssueCustomField.where(:is_for_all => true)
248 221 end
249 222 end
250 223 end
@@ -1,24 +1,26
1 1 <%= javascript_tag do %>
2 2 var operatorLabels = <%= raw_json Query.operators_labels %>;
3 3 var operatorByType = <%= raw_json Query.operators_by_filter_type %>;
4 4 var availableFilters = <%= raw_json query.available_filters_as_json %>;
5 5 var labelDayPlural = <%= raw_json l(:label_day_plural) %>;
6 var allProjects = <%= raw_json query.all_projects_values %>;
6
7 var filtersUrl = <%= raw_json queries_filter_path(:project_id => @query.project.try(:id), :type => @query.type) %>;
8
7 9 $(document).ready(function(){
8 10 initFilters();
9 11 <% query.filters.each do |field, options| %>
10 12 addFilter("<%= field %>", <%= raw_json query.operator_for(field) %>, <%= raw_json query.values_for(field) %>);
11 13 <% end %>
12 14 });
13 15 <% end %>
14 16
15 17 <table id="filters-table">
16 18 </table>
17 19
18 20 <div class="add-filter">
19 21 <%= label_tag('add_filter_select', l(:label_filter_add)) %>
20 22 <%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %>
21 23 </div>
22 24
23 25 <%= hidden_field_tag 'f[]', '' %>
24 26 <% include_calendar_headers_tags %>
@@ -1,388 +1,389
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 Rails.application.routes.draw do
19 19 root :to => 'welcome#index', :as => 'home'
20 20
21 21 match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
22 22 match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post]
23 23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 25 match 'account/activate', :to => 'account#activate', :via => :get
26 26 get 'account/activation_email', :to => 'account#activation_email', :as => 'activation_email'
27 27
28 28 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post, :put, :patch]
29 29 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post, :put, :patch]
30 30 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post, :put, :patch]
31 31 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post, :put, :patch]
32 32
33 33 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
34 34 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
35 35
36 36 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
37 37 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
38 38 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
39 39 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
40 40
41 41 post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message'
42 42 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
43 43 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
44 44 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
45 45
46 46 # Misc issue routes. TODO: move into resources
47 47 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
48 48 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post]
49 49 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get
50 50 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
51 51
52 52 resources :journals, :only => [:edit, :update] do
53 53 member do
54 54 get 'diff'
55 55 end
56 56 end
57 57
58 58 get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
59 59 get '/issues/gantt', :to => 'gantts#show'
60 60
61 61 get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
62 62 get '/issues/calendar', :to => 'calendars#show'
63 63
64 64 get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
65 65 get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
66 66
67 67 get '/issues/imports/new', :to => 'imports#new', :as => 'new_issues_import'
68 68 post '/imports', :to => 'imports#create', :as => 'imports'
69 69 get '/imports/:id', :to => 'imports#show', :as => 'import'
70 70 match '/imports/:id/settings', :to => 'imports#settings', :via => [:get, :post], :as => 'import_settings'
71 71 match '/imports/:id/mapping', :to => 'imports#mapping', :via => [:get, :post], :as => 'import_mapping'
72 72 match '/imports/:id/run', :to => 'imports#run', :via => [:get, :post], :as => 'import_run'
73 73
74 74 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
75 75 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
76 76 match 'my/page', :controller => 'my', :action => 'page', :via => :get
77 77 post 'my/page', :to => 'my#update_page'
78 78 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
79 79 get 'my/api_key', :to => 'my#show_api_key', :as => 'my_api_key'
80 80 post 'my/api_key', :to => 'my#reset_api_key'
81 81 post 'my/rss_key', :to => 'my#reset_rss_key', :as => 'my_rss_key'
82 82 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
83 83 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
84 84 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
85 85 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
86 86 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
87 87
88 88 resources :users do
89 89 resources :memberships, :controller => 'principal_memberships'
90 90 resources :email_addresses, :only => [:index, :create, :update, :destroy]
91 91 end
92 92
93 93 post 'watchers/watch', :to => 'watchers#watch', :as => 'watch'
94 94 delete 'watchers/watch', :to => 'watchers#unwatch'
95 95 get 'watchers/new', :to => 'watchers#new', :as => 'new_watchers'
96 96 post 'watchers', :to => 'watchers#create'
97 97 post 'watchers/append', :to => 'watchers#append'
98 98 delete 'watchers', :to => 'watchers#destroy'
99 99 get 'watchers/autocomplete_for_user', :to => 'watchers#autocomplete_for_user'
100 100 # Specific routes for issue watchers API
101 101 post 'issues/:object_id/watchers', :to => 'watchers#create', :object_type => 'issue'
102 102 delete 'issues/:object_id/watchers/:user_id' => 'watchers#destroy', :object_type => 'issue'
103 103
104 104 resources :projects do
105 105 member do
106 106 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
107 107 post 'modules'
108 108 post 'archive'
109 109 post 'unarchive'
110 110 post 'close'
111 111 post 'reopen'
112 112 match 'copy', :via => [:get, :post]
113 113 end
114 114
115 115 shallow do
116 116 resources :memberships, :controller => 'members' do
117 117 collection do
118 118 get 'autocomplete'
119 119 end
120 120 end
121 121 end
122 122
123 123 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
124 124
125 125 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
126 126 resources :issues, :only => [:index, :new, :create]
127 127 # Used when updating the form of a new issue
128 128 post 'issues/new', :to => 'issues#new'
129 129
130 130 resources :files, :only => [:index, :new, :create]
131 131
132 132 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
133 133 collection do
134 134 put 'close_completed'
135 135 end
136 136 end
137 137 get 'versions.:format', :to => 'versions#index'
138 138 get 'roadmap', :to => 'versions#index', :format => false
139 139 get 'versions', :to => 'versions#index'
140 140
141 141 resources :news, :except => [:show, :edit, :update, :destroy]
142 142 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
143 143 get 'report', :on => :collection
144 144 end
145 145 resources :queries, :only => [:new, :create]
146 146 shallow do
147 147 resources :issue_categories
148 148 end
149 149 resources :documents, :except => [:show, :edit, :update, :destroy]
150 150 resources :boards
151 151 shallow do
152 152 resources :repositories, :except => [:index, :show] do
153 153 member do
154 154 match 'committers', :via => [:get, :post]
155 155 end
156 156 end
157 157 end
158 158
159 159 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
160 160 resources :wiki, :except => [:index, :create], :as => 'wiki_page' do
161 161 member do
162 162 get 'rename'
163 163 post 'rename'
164 164 get 'history'
165 165 get 'diff'
166 166 match 'preview', :via => [:post, :put, :patch]
167 167 post 'protect'
168 168 post 'add_attachment'
169 169 end
170 170 collection do
171 171 get 'export'
172 172 get 'date_index'
173 173 post 'new'
174 174 end
175 175 end
176 176 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
177 177 get 'wiki/:id/:version', :to => 'wiki#show', :constraints => {:version => /\d+/}
178 178 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
179 179 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
180 180 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
181 181 end
182 182
183 183 resources :issues do
184 184 member do
185 185 # Used when updating the form of an existing issue
186 186 patch 'edit', :to => 'issues#edit'
187 187 end
188 188 collection do
189 189 match 'bulk_edit', :via => [:get, :post]
190 190 post 'bulk_update'
191 191 end
192 192 resources :time_entries, :controller => 'timelog', :only => [:new, :create]
193 193 shallow do
194 194 resources :relations, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
195 195 end
196 196 end
197 197 # Used when updating the form of a new issue outside a project
198 198 post '/issues/new', :to => 'issues#new'
199 199 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
200 200
201 201 resources :queries, :except => [:show]
202 get '/queries/filter', :to => 'queries#filter', :as => 'queries_filter'
202 203
203 204 resources :news, :only => [:index, :show, :edit, :update, :destroy]
204 205 match '/news/:id/comments', :to => 'comments#create', :via => :post
205 206 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
206 207
207 208 resources :versions, :only => [:show, :edit, :update, :destroy] do
208 209 post 'status_by', :on => :member
209 210 end
210 211
211 212 resources :documents, :only => [:show, :edit, :update, :destroy] do
212 213 post 'add_attachment', :on => :member
213 214 end
214 215
215 216 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
216 217
217 218 resources :time_entries, :controller => 'timelog', :except => :destroy do
218 219 member do
219 220 # Used when updating the edit form of an existing time entry
220 221 patch 'edit', :to => 'timelog#edit'
221 222 end
222 223 collection do
223 224 get 'report'
224 225 get 'bulk_edit'
225 226 post 'bulk_update'
226 227 end
227 228 end
228 229 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
229 230 # TODO: delete /time_entries for bulk deletion
230 231 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
231 232 # Used to update the new time entry form
232 233 post '/time_entries/new', :to => 'timelog#new'
233 234
234 235 get 'projects/:id/activity', :to => 'activities#index', :as => :project_activity
235 236 get 'activity', :to => 'activities#index'
236 237
237 238 # repositories routes
238 239 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
239 240 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
240 241
241 242 get 'projects/:id/repository/:repository_id/changes(/*path)',
242 243 :to => 'repositories#changes',
243 244 :format => false
244 245
245 246 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
246 247 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
247 248 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
248 249 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
249 250 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
250 251 %w(browse show entry raw annotate diff).each do |action|
251 252 get "projects/:id/repository/:repository_id/revisions/:rev/#{action}(/*path)",
252 253 :controller => 'repositories',
253 254 :action => action,
254 255 :format => false,
255 256 :constraints => {:rev => /[a-z0-9\.\-_]+/}
256 257 end
257 258
258 259 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
259 260 get 'projects/:id/repository/graph', :to => 'repositories#graph'
260 261
261 262 get 'projects/:id/repository/changes(/*path)',
262 263 :to => 'repositories#changes',
263 264 :format => false
264 265
265 266 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
266 267 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
267 268 get 'projects/:id/repository/revision', :to => 'repositories#revision'
268 269 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
269 270 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
270 271 %w(browse show entry raw annotate diff).each do |action|
271 272 get "projects/:id/repository/revisions/:rev/#{action}(/*path)",
272 273 :controller => 'repositories',
273 274 :action => action,
274 275 :format => false,
275 276 :constraints => {:rev => /[a-z0-9\.\-_]+/}
276 277 end
277 278 %w(browse entry raw changes annotate diff).each do |action|
278 279 get "projects/:id/repository/:repository_id/#{action}(/*path)",
279 280 :controller => 'repositories',
280 281 :action => action,
281 282 :format => false
282 283 end
283 284 %w(browse entry raw changes annotate diff).each do |action|
284 285 get "projects/:id/repository/#{action}(/*path)",
285 286 :controller => 'repositories',
286 287 :action => action,
287 288 :format => false
288 289 end
289 290
290 291 get 'projects/:id/repository/:repository_id/show/*path', :to => 'repositories#show', :format => false
291 292 get 'projects/:id/repository/show/*path', :to => 'repositories#show', :format => false
292 293
293 294 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
294 295 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
295 296
296 297 # additional routes for having the file name at the end of url
297 298 get 'attachments/:id/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'named_attachment'
298 299 get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment'
299 300 get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/
300 301 get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail'
301 302 resources :attachments, :only => [:show, :update, :destroy]
302 303 get 'attachments/:object_type/:object_id/edit', :to => 'attachments#edit_all', :as => :object_attachments_edit
303 304 patch 'attachments/:object_type/:object_id', :to => 'attachments#update_all', :as => :object_attachments
304 305
305 306 resources :groups do
306 307 resources :memberships, :controller => 'principal_memberships'
307 308 member do
308 309 get 'autocomplete_for_user'
309 310 end
310 311 end
311 312
312 313 get 'groups/:id/users/new', :to => 'groups#new_users', :id => /\d+/, :as => 'new_group_users'
313 314 post 'groups/:id/users', :to => 'groups#add_users', :id => /\d+/, :as => 'group_users'
314 315 delete 'groups/:id/users/:user_id', :to => 'groups#remove_user', :id => /\d+/, :as => 'group_user'
315 316
316 317 resources :trackers, :except => :show do
317 318 collection do
318 319 match 'fields', :via => [:get, :post]
319 320 end
320 321 end
321 322 resources :issue_statuses, :except => :show do
322 323 collection do
323 324 post 'update_issue_done_ratio'
324 325 end
325 326 end
326 327 resources :custom_fields, :except => :show do
327 328 resources :enumerations, :controller => 'custom_field_enumerations', :except => [:show, :new, :edit]
328 329 put 'enumerations', :to => 'custom_field_enumerations#update_each'
329 330 end
330 331 resources :roles do
331 332 collection do
332 333 match 'permissions', :via => [:get, :post]
333 334 end
334 335 end
335 336 resources :enumerations, :except => :show
336 337 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
337 338
338 339 get 'projects/:id/search', :controller => 'search', :action => 'index'
339 340 get 'search', :controller => 'search', :action => 'index'
340 341
341 342
342 343 get 'mail_handler', :to => 'mail_handler#new'
343 344 post 'mail_handler', :to => 'mail_handler#index'
344 345
345 346 get 'admin', :to => 'admin#index'
346 347 get 'admin/projects', :to => 'admin#projects'
347 348 get 'admin/plugins', :to => 'admin#plugins'
348 349 get 'admin/info', :to => 'admin#info'
349 350 post 'admin/test_email', :to => 'admin#test_email', :as => 'test_email'
350 351 post 'admin/default_configuration', :to => 'admin#default_configuration'
351 352
352 353 resources :auth_sources do
353 354 member do
354 355 get 'test_connection', :as => 'try_connection'
355 356 end
356 357 collection do
357 358 get 'autocomplete_for_new_user'
358 359 end
359 360 end
360 361
361 362 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
362 363 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
363 364 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
364 365 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
365 366 match 'settings', :controller => 'settings', :action => 'index', :via => :get
366 367 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
367 368 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
368 369
369 370 match 'sys/projects', :to => 'sys#projects', :via => :get
370 371 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
371 372 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => [:get, :post]
372 373
373 374 match 'uploads', :to => 'attachments#upload', :via => :post
374 375
375 376 get 'robots.txt', :to => 'welcome#robots'
376 377
377 378 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
378 379 file = File.join(plugin_dir, "config/routes.rb")
379 380 if File.exists?(file)
380 381 begin
381 382 instance_eval File.read(file)
382 383 rescue Exception => e
383 384 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
384 385 exit 1
385 386 end
386 387 end
387 388 end
388 389 end
@@ -1,852 +1,864
1 1 /* Redmine - project management software
2 2 Copyright (C) 2006-2016 Jean-Philippe Lang */
3 3
4 4 function checkAll(id, checked) {
5 5 $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
6 6 }
7 7
8 8 function toggleCheckboxesBySelector(selector) {
9 9 var all_checked = true;
10 10 $(selector).each(function(index) {
11 11 if (!$(this).is(':checked')) { all_checked = false; }
12 12 });
13 13 $(selector).prop('checked', !all_checked);
14 14 }
15 15
16 16 function showAndScrollTo(id, focus) {
17 17 $('#'+id).show();
18 18 if (focus !== null) {
19 19 $('#'+focus).focus();
20 20 }
21 21 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
22 22 }
23 23
24 24 function toggleRowGroup(el) {
25 25 var tr = $(el).parents('tr').first();
26 26 var n = tr.next();
27 27 tr.toggleClass('open');
28 28 while (n.length && !n.hasClass('group')) {
29 29 n.toggle();
30 30 n = n.next('tr');
31 31 }
32 32 }
33 33
34 34 function collapseAllRowGroups(el) {
35 35 var tbody = $(el).parents('tbody').first();
36 36 tbody.children('tr').each(function(index) {
37 37 if ($(this).hasClass('group')) {
38 38 $(this).removeClass('open');
39 39 } else {
40 40 $(this).hide();
41 41 }
42 42 });
43 43 }
44 44
45 45 function expandAllRowGroups(el) {
46 46 var tbody = $(el).parents('tbody').first();
47 47 tbody.children('tr').each(function(index) {
48 48 if ($(this).hasClass('group')) {
49 49 $(this).addClass('open');
50 50 } else {
51 51 $(this).show();
52 52 }
53 53 });
54 54 }
55 55
56 56 function toggleAllRowGroups(el) {
57 57 var tr = $(el).parents('tr').first();
58 58 if (tr.hasClass('open')) {
59 59 collapseAllRowGroups(el);
60 60 } else {
61 61 expandAllRowGroups(el);
62 62 }
63 63 }
64 64
65 65 function toggleFieldset(el) {
66 66 var fieldset = $(el).parents('fieldset').first();
67 67 fieldset.toggleClass('collapsed');
68 68 fieldset.children('div').toggle();
69 69 }
70 70
71 71 function hideFieldset(el) {
72 72 var fieldset = $(el).parents('fieldset').first();
73 73 fieldset.toggleClass('collapsed');
74 74 fieldset.children('div').hide();
75 75 }
76 76
77 77 // columns selection
78 78 function moveOptions(theSelFrom, theSelTo) {
79 79 $(theSelFrom).find('option:selected').detach().prop("selected", false).appendTo($(theSelTo));
80 80 }
81 81
82 82 function moveOptionUp(theSel) {
83 83 $(theSel).find('option:selected').each(function(){
84 84 $(this).prev(':not(:selected)').detach().insertAfter($(this));
85 85 });
86 86 }
87 87
88 88 function moveOptionTop(theSel) {
89 89 $(theSel).find('option:selected').detach().prependTo($(theSel));
90 90 }
91 91
92 92 function moveOptionDown(theSel) {
93 93 $($(theSel).find('option:selected').get().reverse()).each(function(){
94 94 $(this).next(':not(:selected)').detach().insertBefore($(this));
95 95 });
96 96 }
97 97
98 98 function moveOptionBottom(theSel) {
99 99 $(theSel).find('option:selected').detach().appendTo($(theSel));
100 100 }
101 101
102 102 function initFilters() {
103 103 $('#add_filter_select').change(function() {
104 104 addFilter($(this).val(), '', []);
105 105 });
106 106 $('#filters-table td.field input[type=checkbox]').each(function() {
107 107 toggleFilter($(this).val());
108 108 });
109 109 $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
110 110 toggleFilter($(this).val());
111 111 });
112 112 $('#filters-table').on('click', '.toggle-multiselect', function() {
113 113 toggleMultiSelect($(this).siblings('select'));
114 114 });
115 115 $('#filters-table').on('keypress', 'input[type=text]', function(e) {
116 116 if (e.keyCode == 13) $(this).closest('form').submit();
117 117 });
118 118 }
119 119
120 120 function addFilter(field, operator, values) {
121 121 var fieldId = field.replace('.', '_');
122 122 var tr = $('#tr_'+fieldId);
123
124 var filterOptions = availableFilters[field];
125 if (!filterOptions) return;
126
127 if (filterOptions['remote'] && filterOptions['values'] == null) {
128 $.getJSON(filtersUrl, {'name': field}).done(function(data) {
129 filterOptions['values'] = data;
130 addFilter(field, operator, values) ;
131 });
132 return;
133 }
134
123 135 if (tr.length > 0) {
124 136 tr.show();
125 137 } else {
126 138 buildFilterRow(field, operator, values);
127 139 }
128 140 $('#cb_'+fieldId).prop('checked', true);
129 141 toggleFilter(field);
130 142 $('#add_filter_select').val('').find('option').each(function() {
131 143 if ($(this).attr('value') == field) {
132 144 $(this).attr('disabled', true);
133 145 }
134 146 });
135 147 }
136 148
137 function buildFilterRow(field, operator, values) {
149 function buildFilterRow(field, operator, values, loadedValues) {
138 150 var fieldId = field.replace('.', '_');
139 151 var filterTable = $("#filters-table");
140 152 var filterOptions = availableFilters[field];
141 153 if (!filterOptions) return;
142 154 var operators = operatorByType[filterOptions['type']];
143 155 var filterValues = filterOptions['values'];
144 156 var i, select;
145 157
146 158 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
147 159 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
148 160 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
149 161 '<td class="values"></td>'
150 162 );
151 163 filterTable.append(tr);
152 164
153 165 select = tr.find('td.operator select');
154 166 for (i = 0; i < operators.length; i++) {
155 167 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
156 168 if (operators[i] == operator) { option.attr('selected', true); }
157 169 select.append(option);
158 170 }
159 171 select.change(function(){ toggleOperator(field); });
160 172
161 173 switch (filterOptions['type']) {
162 174 case "list":
163 175 case "list_optional":
164 176 case "list_status":
165 177 case "list_subprojects":
166 178 tr.find('td.values').append(
167 179 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
168 180 ' <span class="toggle-multiselect">&nbsp;</span></span>'
169 181 );
170 182 select = tr.find('td.values select');
171 183 if (values.length > 1) { select.attr('multiple', true); }
172 184 for (i = 0; i < filterValues.length; i++) {
173 185 var filterValue = filterValues[i];
174 186 var option = $('<option>');
175 187 if ($.isArray(filterValue)) {
176 188 option.val(filterValue[1]).text(filterValue[0]);
177 189 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
178 190 if (filterValue.length == 3) {
179 191 var optgroup = select.find('optgroup').filter(function(){return $(this).attr('label') == filterValue[2]});
180 192 if (!optgroup.length) {optgroup = $('<optgroup>').attr('label', filterValue[2]);}
181 193 option = optgroup.append(option);
182 194 }
183 195 } else {
184 196 option.val(filterValue).text(filterValue);
185 197 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
186 198 }
187 199 select.append(option);
188 200 }
189 201 break;
190 202 case "date":
191 203 case "date_past":
192 204 tr.find('td.values').append(
193 205 '<span style="display:none;"><input type="date" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
194 206 ' <span style="display:none;"><input type="date" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
195 207 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
196 208 );
197 209 $('#values_'+fieldId+'_1').val(values[0]).datepickerFallback(datepickerOptions);
198 210 $('#values_'+fieldId+'_2').val(values[1]).datepickerFallback(datepickerOptions);
199 211 $('#values_'+fieldId).val(values[0]);
200 212 break;
201 213 case "string":
202 214 case "text":
203 215 tr.find('td.values').append(
204 216 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
205 217 );
206 218 $('#values_'+fieldId).val(values[0]);
207 219 break;
208 220 case "relation":
209 221 tr.find('td.values').append(
210 222 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
211 223 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
212 224 );
213 225 $('#values_'+fieldId).val(values[0]);
214 226 select = tr.find('td.values select');
215 for (i = 0; i < allProjects.length; i++) {
216 var filterValue = allProjects[i];
227 for (i = 0; i < filterValues.length; i++) {
228 var filterValue = filterValues[i];
217 229 var option = $('<option>');
218 230 option.val(filterValue[1]).text(filterValue[0]);
219 231 if (values[0] == filterValue[1]) { option.attr('selected', true); }
220 232 select.append(option);
221 233 }
222 234 break;
223 235 case "integer":
224 236 case "float":
225 237 case "tree":
226 238 tr.find('td.values').append(
227 239 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="14" class="value" /></span>' +
228 240 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="14" class="value" /></span>'
229 241 );
230 242 $('#values_'+fieldId+'_1').val(values[0]);
231 243 $('#values_'+fieldId+'_2').val(values[1]);
232 244 break;
233 245 }
234 246 }
235 247
236 248 function toggleFilter(field) {
237 249 var fieldId = field.replace('.', '_');
238 250 if ($('#cb_' + fieldId).is(':checked')) {
239 251 $("#operators_" + fieldId).show().removeAttr('disabled');
240 252 toggleOperator(field);
241 253 } else {
242 254 $("#operators_" + fieldId).hide().attr('disabled', true);
243 255 enableValues(field, []);
244 256 }
245 257 }
246 258
247 259 function enableValues(field, indexes) {
248 260 var fieldId = field.replace('.', '_');
249 261 $('#tr_'+fieldId+' td.values .value').each(function(index) {
250 262 if ($.inArray(index, indexes) >= 0) {
251 263 $(this).removeAttr('disabled');
252 264 $(this).parents('span').first().show();
253 265 } else {
254 266 $(this).val('');
255 267 $(this).attr('disabled', true);
256 268 $(this).parents('span').first().hide();
257 269 }
258 270
259 271 if ($(this).hasClass('group')) {
260 272 $(this).addClass('open');
261 273 } else {
262 274 $(this).show();
263 275 }
264 276 });
265 277 }
266 278
267 279 function toggleOperator(field) {
268 280 var fieldId = field.replace('.', '_');
269 281 var operator = $("#operators_" + fieldId);
270 282 switch (operator.val()) {
271 283 case "!*":
272 284 case "*":
273 285 case "t":
274 286 case "ld":
275 287 case "w":
276 288 case "lw":
277 289 case "l2w":
278 290 case "m":
279 291 case "lm":
280 292 case "y":
281 293 case "o":
282 294 case "c":
283 295 case "*o":
284 296 case "!o":
285 297 enableValues(field, []);
286 298 break;
287 299 case "><":
288 300 enableValues(field, [0,1]);
289 301 break;
290 302 case "<t+":
291 303 case ">t+":
292 304 case "><t+":
293 305 case "t+":
294 306 case ">t-":
295 307 case "<t-":
296 308 case "><t-":
297 309 case "t-":
298 310 enableValues(field, [2]);
299 311 break;
300 312 case "=p":
301 313 case "=!p":
302 314 case "!p":
303 315 enableValues(field, [1]);
304 316 break;
305 317 default:
306 318 enableValues(field, [0]);
307 319 break;
308 320 }
309 321 }
310 322
311 323 function toggleMultiSelect(el) {
312 324 if (el.attr('multiple')) {
313 325 el.removeAttr('multiple');
314 326 el.attr('size', 1);
315 327 } else {
316 328 el.attr('multiple', true);
317 329 if (el.children().length > 10)
318 330 el.attr('size', 10);
319 331 else
320 332 el.attr('size', 4);
321 333 }
322 334 }
323 335
324 336 function showTab(name, url) {
325 337 $('#tab-content-' + name).parent().find('.tab-content').hide();
326 338 $('#tab-content-' + name).parent().find('div.tabs a').removeClass('selected');
327 339 $('#tab-content-' + name).show();
328 340 $('#tab-' + name).addClass('selected');
329 341 //replaces current URL with the "href" attribute of the current link
330 342 //(only triggered if supported by browser)
331 343 if ("replaceState" in window.history) {
332 344 window.history.replaceState(null, document.title, url);
333 345 }
334 346 return false;
335 347 }
336 348
337 349 function moveTabRight(el) {
338 350 var lis = $(el).parents('div.tabs').first().find('ul').children();
339 351 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
340 352 var tabsWidth = 0;
341 353 var i = 0;
342 354 lis.each(function() {
343 355 if ($(this).is(':visible')) {
344 356 tabsWidth += $(this).outerWidth(true);
345 357 }
346 358 });
347 359 if (tabsWidth < $(el).parents('div.tabs').first().width() - bw) { return; }
348 360 $(el).siblings('.tab-left').removeClass('disabled');
349 361 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
350 362 var w = lis.eq(i).width();
351 363 lis.eq(i).hide();
352 364 if (tabsWidth - w < $(el).parents('div.tabs').first().width() - bw) {
353 365 $(el).addClass('disabled');
354 366 }
355 367 }
356 368
357 369 function moveTabLeft(el) {
358 370 var lis = $(el).parents('div.tabs').first().find('ul').children();
359 371 var i = 0;
360 372 while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
361 373 if (i > 0) {
362 374 lis.eq(i-1).show();
363 375 $(el).siblings('.tab-right').removeClass('disabled');
364 376 }
365 377 if (i <= 1) {
366 378 $(el).addClass('disabled');
367 379 }
368 380 }
369 381
370 382 function displayTabsButtons() {
371 383 var lis;
372 384 var tabsWidth;
373 385 var el;
374 386 var numHidden;
375 387 $('div.tabs').each(function() {
376 388 el = $(this);
377 389 lis = el.find('ul').children();
378 390 tabsWidth = 0;
379 391 numHidden = 0;
380 392 lis.each(function(){
381 393 if ($(this).is(':visible')) {
382 394 tabsWidth += $(this).outerWidth(true);
383 395 } else {
384 396 numHidden++;
385 397 }
386 398 });
387 399 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
388 400 if ((tabsWidth < el.width() - bw) && (lis.length === 0 || lis.first().is(':visible'))) {
389 401 el.find('div.tabs-buttons').hide();
390 402 } else {
391 403 el.find('div.tabs-buttons').show().children('button.tab-left').toggleClass('disabled', numHidden == 0);
392 404 }
393 405 });
394 406 }
395 407
396 408 function setPredecessorFieldsVisibility() {
397 409 var relationType = $('#relation_relation_type');
398 410 if (relationType.val() == "precedes" || relationType.val() == "follows") {
399 411 $('#predecessor_fields').show();
400 412 } else {
401 413 $('#predecessor_fields').hide();
402 414 }
403 415 }
404 416
405 417 function showModal(id, width, title) {
406 418 var el = $('#'+id).first();
407 419 if (el.length === 0 || el.is(':visible')) {return;}
408 420 if (!title) title = el.find('h3.title').text();
409 421 // moves existing modals behind the transparent background
410 422 $(".modal").zIndex(99);
411 423 el.dialog({
412 424 width: width,
413 425 modal: true,
414 426 resizable: false,
415 427 dialogClass: 'modal',
416 428 title: title
417 429 }).on('dialogclose', function(){
418 430 $(".modal").zIndex(101);
419 431 });
420 432 el.find("input[type=text], input[type=submit]").first().focus();
421 433 }
422 434
423 435 function hideModal(el) {
424 436 var modal;
425 437 if (el) {
426 438 modal = $(el).parents('.ui-dialog-content');
427 439 } else {
428 440 modal = $('#ajax-modal');
429 441 }
430 442 modal.dialog("close");
431 443 }
432 444
433 445 function submitPreview(url, form, target) {
434 446 $.ajax({
435 447 url: url,
436 448 type: 'post',
437 449 data: $('#'+form).serialize(),
438 450 success: function(data){
439 451 $('#'+target).html(data);
440 452 }
441 453 });
442 454 }
443 455
444 456 function collapseScmEntry(id) {
445 457 $('.'+id).each(function() {
446 458 if ($(this).hasClass('open')) {
447 459 collapseScmEntry($(this).attr('id'));
448 460 }
449 461 $(this).hide();
450 462 });
451 463 $('#'+id).removeClass('open');
452 464 }
453 465
454 466 function expandScmEntry(id) {
455 467 $('.'+id).each(function() {
456 468 $(this).show();
457 469 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
458 470 expandScmEntry($(this).attr('id'));
459 471 }
460 472 });
461 473 $('#'+id).addClass('open');
462 474 }
463 475
464 476 function scmEntryClick(id, url) {
465 477 var el = $('#'+id);
466 478 if (el.hasClass('open')) {
467 479 collapseScmEntry(id);
468 480 el.addClass('collapsed');
469 481 return false;
470 482 } else if (el.hasClass('loaded')) {
471 483 expandScmEntry(id);
472 484 el.removeClass('collapsed');
473 485 return false;
474 486 }
475 487 if (el.hasClass('loading')) {
476 488 return false;
477 489 }
478 490 el.addClass('loading');
479 491 $.ajax({
480 492 url: url,
481 493 success: function(data) {
482 494 el.after(data);
483 495 el.addClass('open').addClass('loaded').removeClass('loading');
484 496 }
485 497 });
486 498 return true;
487 499 }
488 500
489 501 function randomKey(size) {
490 502 var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
491 503 var key = '';
492 504 for (var i = 0; i < size; i++) {
493 505 key += chars.charAt(Math.floor(Math.random() * chars.length));
494 506 }
495 507 return key;
496 508 }
497 509
498 510 function updateIssueFrom(url, el) {
499 511 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
500 512 $(this).data('valuebeforeupdate', $(this).val());
501 513 });
502 514 if (el) {
503 515 $("#form_update_triggered_by").val($(el).attr('id'));
504 516 }
505 517 return $.ajax({
506 518 url: url,
507 519 type: 'post',
508 520 data: $('#issue-form').serialize()
509 521 });
510 522 }
511 523
512 524 function replaceIssueFormWith(html){
513 525 var replacement = $(html);
514 526 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
515 527 var object_id = $(this).attr('id');
516 528 if (object_id && $(this).data('valuebeforeupdate')!=$(this).val()) {
517 529 replacement.find('#'+object_id).val($(this).val());
518 530 }
519 531 });
520 532 $('#all_attributes').empty();
521 533 $('#all_attributes').prepend(replacement);
522 534 }
523 535
524 536 function updateBulkEditFrom(url) {
525 537 $.ajax({
526 538 url: url,
527 539 type: 'post',
528 540 data: $('#bulk_edit_form').serialize()
529 541 });
530 542 }
531 543
532 544 function observeAutocompleteField(fieldId, url, options) {
533 545 $(document).ready(function() {
534 546 $('#'+fieldId).autocomplete($.extend({
535 547 source: url,
536 548 minLength: 2,
537 549 position: {collision: "flipfit"},
538 550 search: function(){$('#'+fieldId).addClass('ajax-loading');},
539 551 response: function(){$('#'+fieldId).removeClass('ajax-loading');}
540 552 }, options));
541 553 $('#'+fieldId).addClass('autocomplete');
542 554 });
543 555 }
544 556
545 557 function observeSearchfield(fieldId, targetId, url) {
546 558 $('#'+fieldId).each(function() {
547 559 var $this = $(this);
548 560 $this.addClass('autocomplete');
549 561 $this.attr('data-value-was', $this.val());
550 562 var check = function() {
551 563 var val = $this.val();
552 564 if ($this.attr('data-value-was') != val){
553 565 $this.attr('data-value-was', val);
554 566 $.ajax({
555 567 url: url,
556 568 type: 'get',
557 569 data: {q: $this.val()},
558 570 success: function(data){ if(targetId) $('#'+targetId).html(data); },
559 571 beforeSend: function(){ $this.addClass('ajax-loading'); },
560 572 complete: function(){ $this.removeClass('ajax-loading'); }
561 573 });
562 574 }
563 575 };
564 576 var reset = function() {
565 577 if (timer) {
566 578 clearInterval(timer);
567 579 timer = setInterval(check, 300);
568 580 }
569 581 };
570 582 var timer = setInterval(check, 300);
571 583 $this.bind('keyup click mousemove', reset);
572 584 });
573 585 }
574 586
575 587 $(document).ready(function(){
576 588 $(".drdn .autocomplete").val('');
577 589
578 590 $(".drdn-trigger").click(function(e){
579 591 var drdn = $(this).closest(".drdn");
580 592 if (drdn.hasClass("expanded")) {
581 593 drdn.removeClass("expanded");
582 594 } else {
583 595 $(".drdn").removeClass("expanded");
584 596 drdn.addClass("expanded");
585 597 if (!isMobile()) {
586 598 drdn.find(".autocomplete").focus();
587 599 }
588 600 e.stopPropagation();
589 601 }
590 602 });
591 603 $(document).click(function(e){
592 604 if ($(e.target).closest(".drdn").length < 1) {
593 605 $(".drdn.expanded").removeClass("expanded");
594 606 }
595 607 });
596 608
597 609 observeSearchfield('projects-quick-search', null, $('#projects-quick-search').data('automcomplete-url'));
598 610
599 611 $(".drdn-content").keydown(function(event){
600 612 var items = $(this).find(".drdn-items");
601 613 var focused = items.find("a:focus");
602 614 switch (event.which) {
603 615 case 40: //down
604 616 if (focused.length > 0) {
605 617 focused.nextAll("a").first().focus();;
606 618 } else {
607 619 items.find("a").first().focus();;
608 620 }
609 621 event.preventDefault();
610 622 break;
611 623 case 38: //up
612 624 if (focused.length > 0) {
613 625 var prev = focused.prevAll("a");
614 626 if (prev.length > 0) {
615 627 prev.first().focus();
616 628 } else {
617 629 $(this).find(".autocomplete").focus();
618 630 }
619 631 event.preventDefault();
620 632 }
621 633 break;
622 634 case 35: //end
623 635 if (focused.length > 0) {
624 636 focused.nextAll("a").last().focus();
625 637 event.preventDefault();
626 638 }
627 639 break;
628 640 case 36: //home
629 641 if (focused.length > 0) {
630 642 focused.prevAll("a").last().focus();
631 643 event.preventDefault();
632 644 }
633 645 break;
634 646 }
635 647 });
636 648 });
637 649
638 650 function beforeShowDatePicker(input, inst) {
639 651 var default_date = null;
640 652 switch ($(input).attr("id")) {
641 653 case "issue_start_date" :
642 654 if ($("#issue_due_date").size() > 0) {
643 655 default_date = $("#issue_due_date").val();
644 656 }
645 657 break;
646 658 case "issue_due_date" :
647 659 if ($("#issue_start_date").size() > 0) {
648 660 var start_date = $("#issue_start_date").val();
649 661 if (start_date != "") {
650 662 start_date = new Date(Date.parse(start_date));
651 663 if (start_date > new Date()) {
652 664 default_date = $("#issue_start_date").val();
653 665 }
654 666 }
655 667 }
656 668 break;
657 669 }
658 670 $(input).datepickerFallback("option", "defaultDate", default_date);
659 671 }
660 672
661 673 (function($){
662 674 $.fn.positionedItems = function(sortableOptions, options){
663 675 var settings = $.extend({
664 676 firstPosition: 1
665 677 }, options );
666 678
667 679 return this.sortable($.extend({
668 680 axis: 'y',
669 681 handle: ".sort-handle",
670 682 helper: function(event, ui){
671 683 ui.children('td').each(function(){
672 684 $(this).width($(this).width());
673 685 });
674 686 return ui;
675 687 },
676 688 update: function(event, ui) {
677 689 var sortable = $(this);
678 690 var handle = ui.item.find(".sort-handle").addClass("ajax-loading");
679 691 var url = handle.data("reorder-url");
680 692 var param = handle.data("reorder-param");
681 693 var data = {};
682 694 data[param] = {position: ui.item.index() + settings['firstPosition']};
683 695 $.ajax({
684 696 url: url,
685 697 type: 'put',
686 698 dataType: 'script',
687 699 data: data,
688 700 success: function(data){
689 701 sortable.children(":even").removeClass("even").addClass("odd");
690 702 sortable.children(":odd").removeClass("odd").addClass("even");
691 703 },
692 704 error: function(jqXHR, textStatus, errorThrown){
693 705 alert(jqXHR.status);
694 706 sortable.sortable("cancel");
695 707 },
696 708 complete: function(jqXHR, textStatus, errorThrown){
697 709 handle.removeClass("ajax-loading");
698 710 }
699 711 });
700 712 },
701 713 }, sortableOptions));
702 714 }
703 715 }( jQuery ));
704 716
705 717 function initMyPageSortable(list, url) {
706 718 $('#list-'+list).sortable({
707 719 connectWith: '.block-receiver',
708 720 tolerance: 'pointer',
709 721 update: function(){
710 722 $.ajax({
711 723 url: url,
712 724 type: 'post',
713 725 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
714 726 });
715 727 }
716 728 });
717 729 $("#list-top, #list-left, #list-right").disableSelection();
718 730 }
719 731
720 732 var warnLeavingUnsavedMessage;
721 733 function warnLeavingUnsaved(message) {
722 734 warnLeavingUnsavedMessage = message;
723 735 $(document).on('submit', 'form', function(){
724 736 $('textarea').removeData('changed');
725 737 });
726 738 $(document).on('change', 'textarea', function(){
727 739 $(this).data('changed', 'changed');
728 740 });
729 741 window.onbeforeunload = function(){
730 742 var warn = false;
731 743 $('textarea').blur().each(function(){
732 744 if ($(this).data('changed')) {
733 745 warn = true;
734 746 }
735 747 });
736 748 if (warn) {return warnLeavingUnsavedMessage;}
737 749 };
738 750 }
739 751
740 752 function setupAjaxIndicator() {
741 753 $(document).bind('ajaxSend', function(event, xhr, settings) {
742 754 if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
743 755 $('#ajax-indicator').show();
744 756 }
745 757 });
746 758 $(document).bind('ajaxStop', function() {
747 759 $('#ajax-indicator').hide();
748 760 });
749 761 }
750 762
751 763 function setupTabs() {
752 764 if($('.tabs').length > 0) {
753 765 displayTabsButtons();
754 766 $(window).resize(displayTabsButtons);
755 767 }
756 768 }
757 769
758 770 function hideOnLoad() {
759 771 $('.hol').hide();
760 772 }
761 773
762 774 function addFormObserversForDoubleSubmit() {
763 775 $('form[method=post]').each(function() {
764 776 if (!$(this).hasClass('multiple-submit')) {
765 777 $(this).submit(function(form_submission) {
766 778 if ($(form_submission.target).attr('data-submitted')) {
767 779 form_submission.preventDefault();
768 780 } else {
769 781 $(form_submission.target).attr('data-submitted', true);
770 782 }
771 783 });
772 784 }
773 785 });
774 786 }
775 787
776 788 function defaultFocus(){
777 789 if (($('#content :focus').length == 0) && (window.location.hash == '')) {
778 790 $('#content input[type=text], #content textarea').first().focus();
779 791 }
780 792 }
781 793
782 794 function blockEventPropagation(event) {
783 795 event.stopPropagation();
784 796 event.preventDefault();
785 797 }
786 798
787 799 function toggleDisabledOnChange() {
788 800 var checked = $(this).is(':checked');
789 801 $($(this).data('disables')).attr('disabled', checked);
790 802 $($(this).data('enables')).attr('disabled', !checked);
791 803 $($(this).data('shows')).toggle(checked);
792 804 }
793 805 function toggleDisabledInit() {
794 806 $('input[data-disables], input[data-enables], input[data-shows]').each(toggleDisabledOnChange);
795 807 }
796 808
797 809 function toggleNewObjectDropdown() {
798 810 var dropdown = $('#new-object + ul.menu-children');
799 811 if(dropdown.hasClass('visible')){
800 812 dropdown.removeClass('visible');
801 813 }else{
802 814 dropdown.addClass('visible');
803 815 }
804 816 }
805 817
806 818 (function ( $ ) {
807 819
808 820 // detect if native date input is supported
809 821 var nativeDateInputSupported = true;
810 822
811 823 var input = document.createElement('input');
812 824 input.setAttribute('type','date');
813 825 if (input.type === 'text') {
814 826 nativeDateInputSupported = false;
815 827 }
816 828
817 829 var notADateValue = 'not-a-date';
818 830 input.setAttribute('value', notADateValue);
819 831 if (input.value === notADateValue) {
820 832 nativeDateInputSupported = false;
821 833 }
822 834
823 835 $.fn.datepickerFallback = function( options ) {
824 836 if (nativeDateInputSupported) {
825 837 return this;
826 838 } else {
827 839 return this.datepicker( options );
828 840 }
829 841 };
830 842 }( jQuery ));
831 843
832 844 $(document).ready(function(){
833 845 $('#content').on('change', 'input[data-disables], input[data-enables], input[data-shows]', toggleDisabledOnChange);
834 846 toggleDisabledInit();
835 847 });
836 848
837 849 function keepAnchorOnSignIn(form){
838 850 var hash = decodeURIComponent(self.document.location.hash);
839 851 if (hash) {
840 852 if (hash.indexOf("#") === -1) {
841 853 hash = "#" + hash;
842 854 }
843 855 form.action = form.action + hash;
844 856 }
845 857 return true;
846 858 }
847 859
848 860 $(document).ready(setupAjaxIndicator);
849 861 $(document).ready(hideOnLoad);
850 862 $(document).ready(addFormObserversForDoubleSubmit);
851 863 $(document).ready(defaultFocus);
852 864 $(document).ready(setupTabs);
@@ -1,400 +1,425
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class QueriesControllerTest < Redmine::ControllerTest
21 fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules
21 fixtures :projects, :enabled_modules,
22 :users, :email_addresses,
23 :members, :member_roles, :roles,
24 :trackers, :issue_statuses, :issue_categories, :enumerations, :versions,
25 :issues, :custom_fields, :custom_values,
26 :queries
22 27
23 28 def setup
24 29 User.current = nil
25 30 end
26 31
27 32 def test_index
28 33 get :index
29 34 # HTML response not implemented
30 35 assert_response 406
31 36 end
32 37
33 38 def test_new_project_query
34 39 @request.session[:user_id] = 2
35 40 get :new, :project_id => 1
36 41 assert_response :success
37 42
38 43 assert_select 'input[name=?][value="0"][checked=checked]', 'query[visibility]'
39 44 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked]):not([disabled])'
40 45 assert_select 'select[name=?]', 'c[]' do
41 46 assert_select 'option[value=tracker]'
42 47 assert_select 'option[value=subject]'
43 48 end
44 49 end
45 50
46 51 def test_new_global_query
47 52 @request.session[:user_id] = 2
48 53 get :new
49 54 assert_response :success
50 55
51 56 assert_select 'input[name=?]', 'query[visibility]', 0
52 57 assert_select 'input[name=query_is_for_all][type=checkbox][checked]:not([disabled])'
53 58 end
54 59
55 60 def test_new_on_invalid_project
56 61 @request.session[:user_id] = 2
57 62 get :new, :project_id => 'invalid'
58 63 assert_response 404
59 64 end
60 65
61 66 def test_new_time_entry_query
62 67 @request.session[:user_id] = 2
63 68 get :new, :project_id => 1, :type => 'TimeEntryQuery'
64 69 assert_response :success
65 70 assert_select 'input[name=type][value=?]', 'TimeEntryQuery'
66 71 end
67 72
68 73 def test_create_project_public_query
69 74 @request.session[:user_id] = 2
70 75 post :create,
71 76 :project_id => 'ecookbook',
72 77 :default_columns => '1',
73 78 :f => ["status_id", "assigned_to_id"],
74 79 :op => {"assigned_to_id" => "=", "status_id" => "o"},
75 80 :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
76 81 :query => {"name" => "test_new_project_public_query", "visibility" => "2"}
77 82
78 83 q = Query.find_by_name('test_new_project_public_query')
79 84 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
80 85 assert q.is_public?
81 86 assert q.has_default_columns?
82 87 assert q.valid?
83 88 end
84 89
85 90 def test_create_project_private_query
86 91 @request.session[:user_id] = 3
87 92 post :create,
88 93 :project_id => 'ecookbook',
89 94 :default_columns => '1',
90 95 :fields => ["status_id", "assigned_to_id"],
91 96 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
92 97 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
93 98 :query => {"name" => "test_new_project_private_query", "visibility" => "0"}
94 99
95 100 q = Query.find_by_name('test_new_project_private_query')
96 101 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
97 102 assert !q.is_public?
98 103 assert q.has_default_columns?
99 104 assert q.valid?
100 105 end
101 106
102 107 def test_create_project_roles_query
103 108 @request.session[:user_id] = 2
104 109 post :create,
105 110 :project_id => 'ecookbook',
106 111 :default_columns => '1',
107 112 :fields => ["status_id", "assigned_to_id"],
108 113 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
109 114 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
110 115 :query => {"name" => "test_create_project_roles_query", "visibility" => "1", "role_ids" => ["1", "2", ""]}
111 116
112 117 q = Query.find_by_name('test_create_project_roles_query')
113 118 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
114 119 assert_equal Query::VISIBILITY_ROLES, q.visibility
115 120 assert_equal [1, 2], q.roles.ids.sort
116 121 end
117 122
118 123 def test_create_global_private_query_with_custom_columns
119 124 @request.session[:user_id] = 3
120 125 post :create,
121 126 :fields => ["status_id", "assigned_to_id"],
122 127 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
123 128 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
124 129 :query => {"name" => "test_new_global_private_query", "visibility" => "0"},
125 130 :c => ["", "tracker", "subject", "priority", "category"]
126 131
127 132 q = Query.find_by_name('test_new_global_private_query')
128 133 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
129 134 assert !q.is_public?
130 135 assert !q.has_default_columns?
131 136 assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
132 137 assert q.valid?
133 138 end
134 139
135 140 def test_create_global_query_with_custom_filters
136 141 @request.session[:user_id] = 3
137 142 post :create,
138 143 :fields => ["assigned_to_id"],
139 144 :operators => {"assigned_to_id" => "="},
140 145 :values => { "assigned_to_id" => ["me"]},
141 146 :query => {"name" => "test_new_global_query"}
142 147
143 148 q = Query.find_by_name('test_new_global_query')
144 149 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
145 150 assert !q.is_public?
146 151 assert !q.has_filter?(:status_id)
147 152 assert_equal ['assigned_to_id'], q.filters.keys
148 153 assert q.valid?
149 154 end
150 155
151 156 def test_create_with_sort
152 157 @request.session[:user_id] = 1
153 158 post :create,
154 159 :default_columns => '1',
155 160 :operators => {"status_id" => "o"},
156 161 :values => {"status_id" => ["1"]},
157 162 :query => {:name => "test_new_with_sort",
158 163 :visibility => "2",
159 164 :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
160 165
161 166 query = Query.find_by_name("test_new_with_sort")
162 167 assert_not_nil query
163 168 assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
164 169 end
165 170
166 171 def test_create_with_failure
167 172 @request.session[:user_id] = 2
168 173 assert_no_difference '::Query.count' do
169 174 post :create, :project_id => 'ecookbook', :query => {:name => ''}
170 175 end
171 176 assert_response :success
172 177
173 178 assert_select 'input[name=?]', 'query[name]'
174 179 end
175 180
176 181 def test_create_global_query_from_gantt
177 182 @request.session[:user_id] = 1
178 183 assert_difference 'IssueQuery.count' do
179 184 post :create,
180 185 :gantt => 1,
181 186 :operators => {"status_id" => "o"},
182 187 :values => {"status_id" => ["1"]},
183 188 :query => {:name => "test_create_from_gantt",
184 189 :draw_relations => '1',
185 190 :draw_progress_line => '1'}
186 191 assert_response 302
187 192 end
188 193 query = IssueQuery.order('id DESC').first
189 194 assert_redirected_to "/issues/gantt?query_id=#{query.id}"
190 195 assert_equal true, query.draw_relations
191 196 assert_equal true, query.draw_progress_line
192 197 end
193 198
194 199 def test_create_project_query_from_gantt
195 200 @request.session[:user_id] = 1
196 201 assert_difference 'IssueQuery.count' do
197 202 post :create,
198 203 :project_id => 'ecookbook',
199 204 :gantt => 1,
200 205 :operators => {"status_id" => "o"},
201 206 :values => {"status_id" => ["1"]},
202 207 :query => {:name => "test_create_from_gantt",
203 208 :draw_relations => '0',
204 209 :draw_progress_line => '0'}
205 210 assert_response 302
206 211 end
207 212 query = IssueQuery.order('id DESC').first
208 213 assert_redirected_to "/projects/ecookbook/issues/gantt?query_id=#{query.id}"
209 214 assert_equal false, query.draw_relations
210 215 assert_equal false, query.draw_progress_line
211 216 end
212 217
213 218 def test_create_project_public_query_should_force_private_without_manage_public_queries_permission
214 219 @request.session[:user_id] = 3
215 220 query = new_record(Query) do
216 221 post :create,
217 222 :project_id => 'ecookbook',
218 223 :query => {"name" => "name", "visibility" => "2"}
219 224 assert_response 302
220 225 end
221 226 assert_not_nil query.project
222 227 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
223 228 end
224 229
225 230 def test_create_global_public_query_should_force_private_without_manage_public_queries_permission
226 231 @request.session[:user_id] = 3
227 232 query = new_record(Query) do
228 233 post :create,
229 234 :project_id => 'ecookbook', :query_is_for_all => '1',
230 235 :query => {"name" => "name", "visibility" => "2"}
231 236 assert_response 302
232 237 end
233 238 assert_nil query.project
234 239 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
235 240 end
236 241
237 242 def test_create_project_public_query_with_manage_public_queries_permission
238 243 @request.session[:user_id] = 2
239 244 query = new_record(Query) do
240 245 post :create,
241 246 :project_id => 'ecookbook',
242 247 :query => {"name" => "name", "visibility" => "2"}
243 248 assert_response 302
244 249 end
245 250 assert_not_nil query.project
246 251 assert_equal Query::VISIBILITY_PUBLIC, query.visibility
247 252 end
248 253
249 254 def test_create_global_public_query_should_force_private_with_manage_public_queries_permission
250 255 @request.session[:user_id] = 2
251 256 query = new_record(Query) do
252 257 post :create,
253 258 :project_id => 'ecookbook', :query_is_for_all => '1',
254 259 :query => {"name" => "name", "visibility" => "2"}
255 260 assert_response 302
256 261 end
257 262 assert_nil query.project
258 263 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
259 264 end
260 265
261 266 def test_create_global_public_query_by_admin
262 267 @request.session[:user_id] = 1
263 268 query = new_record(Query) do
264 269 post :create,
265 270 :project_id => 'ecookbook', :query_is_for_all => '1',
266 271 :query => {"name" => "name", "visibility" => "2"}
267 272 assert_response 302
268 273 end
269 274 assert_nil query.project
270 275 assert_equal Query::VISIBILITY_PUBLIC, query.visibility
271 276 end
272 277
273 278 def test_create_project_public_time_entry_query
274 279 @request.session[:user_id] = 2
275 280
276 281 q = new_record(TimeEntryQuery) do
277 282 post :create,
278 283 :project_id => 'ecookbook',
279 284 :type => 'TimeEntryQuery',
280 285 :default_columns => '1',
281 286 :f => ["spent_on"],
282 287 :op => {"spent_on" => "="},
283 288 :v => { "spent_on" => ["2016-07-14"]},
284 289 :query => {"name" => "test_new_project_public_query", "visibility" => "2"}
285 290 end
286 291
287 292 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => 'ecookbook', :query_id => q.id
288 293 assert q.is_public?
289 294 assert q.has_default_columns?
290 295 assert q.valid?
291 296 end
292 297
293 298 def test_edit_global_public_query
294 299 @request.session[:user_id] = 1
295 300 get :edit, :id => 4
296 301 assert_response :success
297 302
298 303 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
299 304 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked]'
300 305 end
301 306
302 307 def test_edit_global_private_query
303 308 @request.session[:user_id] = 3
304 309 get :edit, :id => 3
305 310 assert_response :success
306 311
307 312 assert_select 'input[name=?]', 'query[visibility]', 0
308 313 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked]'
309 314 end
310 315
311 316 def test_edit_project_private_query
312 317 @request.session[:user_id] = 3
313 318 get :edit, :id => 2
314 319 assert_response :success
315 320
316 321 assert_select 'input[name=?]', 'query[visibility]', 0
317 322 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked])'
318 323 end
319 324
320 325 def test_edit_project_public_query
321 326 @request.session[:user_id] = 2
322 327 get :edit, :id => 1
323 328 assert_response :success
324 329
325 330 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
326 331 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked])'
327 332 end
328 333
329 334 def test_edit_sort_criteria
330 335 @request.session[:user_id] = 1
331 336 get :edit, :id => 5
332 337 assert_response :success
333 338
334 339 assert_select 'select[name=?]', 'query[sort_criteria][0][]' do
335 340 assert_select 'option[value=priority][selected=selected]'
336 341 assert_select 'option[value=desc][selected=selected]'
337 342 end
338 343 end
339 344
340 345 def test_edit_invalid_query
341 346 @request.session[:user_id] = 2
342 347 get :edit, :id => 99
343 348 assert_response 404
344 349 end
345 350
346 351 def test_udpate_global_private_query
347 352 @request.session[:user_id] = 3
348 353 put :update,
349 354 :id => 3,
350 355 :default_columns => '1',
351 356 :fields => ["status_id", "assigned_to_id"],
352 357 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
353 358 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
354 359 :query => {"name" => "test_edit_global_private_query", "visibility" => "2"}
355 360
356 361 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
357 362 q = Query.find_by_name('test_edit_global_private_query')
358 363 assert !q.is_public?
359 364 assert q.has_default_columns?
360 365 assert q.valid?
361 366 end
362 367
363 368 def test_update_global_public_query
364 369 @request.session[:user_id] = 1
365 370 put :update,
366 371 :id => 4,
367 372 :default_columns => '1',
368 373 :fields => ["status_id", "assigned_to_id"],
369 374 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
370 375 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
371 376 :query => {"name" => "test_edit_global_public_query", "visibility" => "2"}
372 377
373 378 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
374 379 q = Query.find_by_name('test_edit_global_public_query')
375 380 assert q.is_public?
376 381 assert q.has_default_columns?
377 382 assert q.valid?
378 383 end
379 384
380 385 def test_update_with_failure
381 386 @request.session[:user_id] = 1
382 387 put :update, :id => 4, :query => {:name => ''}
383 388 assert_response :success
384 389 assert_select_error /Name cannot be blank/
385 390 end
386 391
387 392 def test_destroy
388 393 @request.session[:user_id] = 2
389 394 delete :destroy, :id => 1
390 395 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
391 396 assert_nil Query.find_by_id(1)
392 397 end
393 398
394 399 def test_backslash_should_be_escaped_in_filters
395 400 @request.session[:user_id] = 2
396 401 get :new, :subject => 'foo/bar'
397 402 assert_response :success
398 403 assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body
399 404 end
405
406 def test_filter_with_project_id_should_return_filter_values
407 @request.session[:user_id] = 2
408 get :filter, :project_id => 1, :name => 'fixed_version_id'
409
410 assert_response :success
411 assert_equal 'application/json', response.content_type
412 json = ActiveSupport::JSON.decode(response.body)
413 assert_include ["eCookbook - 2.0", "3", "open"], json
414 end
415
416 def test_filter_without_project_id_should_return_filter_values
417 @request.session[:user_id] = 2
418 get :filter, :name => 'fixed_version_id'
419
420 assert_response :success
421 assert_equal 'application/json', response.content_type
422 json = ActiveSupport::JSON.decode(response.body)
423 assert_include ["OnlineStore - Systemwide visible version", "7", "open"], json
424 end
400 425 end
@@ -1,34 +1,35
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class RoutingQueriesTest < Redmine::RoutingTest
21 21 def test_queries
22 22 should_route 'GET /queries/new' => 'queries#new'
23 23 should_route 'POST /queries' => 'queries#create'
24 should_route 'GET /queries/filter' => 'queries#filter'
24 25
25 26 should_route 'GET /queries/1/edit' => 'queries#edit', :id => '1'
26 27 should_route 'PUT /queries/1' => 'queries#update', :id => '1'
27 28 should_route 'DELETE /queries/1' => 'queries#destroy', :id => '1'
28 29 end
29 30
30 31 def test_queries_scoped_under_project
31 32 should_route 'GET /projects/foo/queries/new' => 'queries#new', :project_id => 'foo'
32 33 should_route 'POST /projects/foo/queries' => 'queries#create', :project_id => 'foo'
33 34 end
34 35 end
@@ -1,1813 +1,1835
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21
22 22 class QueryTest < ActiveSupport::TestCase
23 23 include Redmine::I18n
24 24
25 25 fixtures :projects, :enabled_modules, :users, :members,
26 26 :member_roles, :roles, :trackers, :issue_statuses,
27 27 :issue_categories, :enumerations, :issues,
28 28 :watchers, :custom_fields, :custom_values, :versions,
29 29 :queries,
30 30 :projects_trackers,
31 31 :custom_fields_trackers,
32 32 :workflows
33 33
34 34 def setup
35 35 User.current = nil
36 36 end
37 37
38 38 def test_query_with_roles_visibility_should_validate_roles
39 39 set_language_if_valid 'en'
40 40 query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES)
41 41 assert !query.save
42 42 assert_include "Roles cannot be blank", query.errors.full_messages
43 43 query.role_ids = [1, 2]
44 44 assert query.save
45 45 end
46 46
47 47 def test_changing_roles_visibility_should_clear_roles
48 48 query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2])
49 49 assert_equal 2, query.roles.count
50 50
51 51 query.visibility = IssueQuery::VISIBILITY_PUBLIC
52 52 query.save!
53 53 assert_equal 0, query.roles.count
54 54 end
55 55
56 56 def test_available_filters_should_be_ordered
57 57 set_language_if_valid 'en'
58 58 query = IssueQuery.new
59 59 assert_equal 0, query.available_filters.keys.index('status_id')
60 60 expected_order = [
61 61 "Status",
62 62 "Project",
63 63 "Tracker",
64 64 "Priority"
65 65 ]
66 66 assert_equal expected_order,
67 67 (query.available_filters.values.map{|v| v[:name]} & expected_order)
68 68 end
69 69
70 70 def test_available_filters_with_custom_fields_should_be_ordered
71 71 set_language_if_valid 'en'
72 72 UserCustomField.create!(
73 73 :name => 'order test', :field_format => 'string',
74 74 :is_for_all => true, :is_filter => true
75 75 )
76 76 query = IssueQuery.new
77 77 expected_order = [
78 78 "Searchable field",
79 79 "Database",
80 80 "Project's Development status",
81 81 "Author's order test",
82 82 "Assignee's order test"
83 83 ]
84 84 assert_equal expected_order,
85 85 (query.available_filters.values.map{|v| v[:name]} & expected_order)
86 86 end
87 87
88 88 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
89 89 query = IssueQuery.new(:project => nil, :name => '_')
90 90 assert query.available_filters.has_key?('cf_1')
91 91 assert !query.available_filters.has_key?('cf_3')
92 92 end
93 93
94 94 def test_system_shared_versions_should_be_available_in_global_queries
95 95 Version.find(2).update_attribute :sharing, 'system'
96 96 query = IssueQuery.new(:project => nil, :name => '_')
97 97 assert query.available_filters.has_key?('fixed_version_id')
98 98 assert query.available_filters['fixed_version_id'][:values].detect {|v| v[1] == '2'}
99 99 end
100 100
101 101 def test_project_filter_in_global_queries
102 102 query = IssueQuery.new(:project => nil, :name => '_')
103 103 project_filter = query.available_filters["project_id"]
104 104 assert_not_nil project_filter
105 105 project_ids = project_filter[:values].map{|p| p[1]}
106 106 assert project_ids.include?("1") #public project
107 107 assert !project_ids.include?("2") #private project user cannot see
108 108 end
109 109
110 110 def test_available_filters_should_not_include_fields_disabled_on_all_trackers
111 111 Tracker.all.each do |tracker|
112 112 tracker.core_fields = Tracker::CORE_FIELDS - ['start_date']
113 113 tracker.save!
114 114 end
115 115
116 116 query = IssueQuery.new(:name => '_')
117 117 assert_include 'due_date', query.available_filters
118 118 assert_not_include 'start_date', query.available_filters
119 119 end
120 120
121 def test_filter_values_without_project_should_be_arrays
122 q = IssueQuery.new
123 assert_nil q.project
124
125 q.available_filters.each do |name, filter|
126 values = filter.values
127 assert (values.nil? || values.is_a?(Array)),
128 "#values for #{name} filter returned a #{values.class.name}"
129 end
130 end
131
132 def test_filter_values_with_project_should_be_arrays
133 q = IssueQuery.new(:project => Project.find(1))
134 assert_not_nil q.project
135
136 q.available_filters.each do |name, filter|
137 values = filter.values
138 assert (values.nil? || values.is_a?(Array)),
139 "#values for #{name} filter returned a #{values.class.name}"
140 end
141 end
142
121 143 def find_issues_with_query(query)
122 144 Issue.joins(:status, :tracker, :project, :priority).where(
123 145 query.statement
124 146 ).to_a
125 147 end
126 148
127 149 def assert_find_issues_with_query_is_successful(query)
128 150 assert_nothing_raised do
129 151 find_issues_with_query(query)
130 152 end
131 153 end
132 154
133 155 def assert_query_statement_includes(query, condition)
134 156 assert_include condition, query.statement
135 157 end
136 158
137 159 def assert_query_result(expected, query)
138 160 assert_nothing_raised do
139 161 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
140 162 assert_equal expected.size, query.issue_count
141 163 end
142 164 end
143 165
144 166 def test_query_should_allow_shared_versions_for_a_project_query
145 167 subproject_version = Version.find(4)
146 168 query = IssueQuery.new(:project => Project.find(1), :name => '_')
147 169 filter = query.available_filters["fixed_version_id"]
148 170 assert_not_nil filter
149 171 assert_include subproject_version.id.to_s, filter[:values].map(&:second)
150 172 end
151 173
152 174 def test_query_with_multiple_custom_fields
153 175 query = IssueQuery.find(1)
154 176 assert query.valid?
155 177 issues = find_issues_with_query(query)
156 178 assert_equal 1, issues.length
157 179 assert_equal Issue.find(3), issues.first
158 180 end
159 181
160 182 def test_operator_none
161 183 query = IssueQuery.new(:project => Project.find(1), :name => '_')
162 184 query.add_filter('fixed_version_id', '!*', [''])
163 185 query.add_filter('cf_1', '!*', [''])
164 186 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
165 187 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
166 188 find_issues_with_query(query)
167 189 end
168 190
169 191 def test_operator_none_for_integer
170 192 query = IssueQuery.new(:project => Project.find(1), :name => '_')
171 193 query.add_filter('estimated_hours', '!*', [''])
172 194 issues = find_issues_with_query(query)
173 195 assert !issues.empty?
174 196 assert issues.all? {|i| !i.estimated_hours}
175 197 end
176 198
177 199 def test_operator_none_for_date
178 200 query = IssueQuery.new(:project => Project.find(1), :name => '_')
179 201 query.add_filter('start_date', '!*', [''])
180 202 issues = find_issues_with_query(query)
181 203 assert !issues.empty?
182 204 assert issues.all? {|i| i.start_date.nil?}
183 205 end
184 206
185 207 def test_operator_none_for_string_custom_field
186 208 CustomField.find(2).update_attribute :default_value, ""
187 209 query = IssueQuery.new(:project => Project.find(1), :name => '_')
188 210 query.add_filter('cf_2', '!*', [''])
189 211 assert query.has_filter?('cf_2')
190 212 issues = find_issues_with_query(query)
191 213 assert !issues.empty?
192 214 assert issues.all? {|i| i.custom_field_value(2).blank?}
193 215 end
194 216
195 217 def test_operator_all
196 218 query = IssueQuery.new(:project => Project.find(1), :name => '_')
197 219 query.add_filter('fixed_version_id', '*', [''])
198 220 query.add_filter('cf_1', '*', [''])
199 221 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
200 222 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
201 223 find_issues_with_query(query)
202 224 end
203 225
204 226 def test_operator_all_for_date
205 227 query = IssueQuery.new(:project => Project.find(1), :name => '_')
206 228 query.add_filter('start_date', '*', [''])
207 229 issues = find_issues_with_query(query)
208 230 assert !issues.empty?
209 231 assert issues.all? {|i| i.start_date.present?}
210 232 end
211 233
212 234 def test_operator_all_for_string_custom_field
213 235 query = IssueQuery.new(:project => Project.find(1), :name => '_')
214 236 query.add_filter('cf_2', '*', [''])
215 237 assert query.has_filter?('cf_2')
216 238 issues = find_issues_with_query(query)
217 239 assert !issues.empty?
218 240 assert issues.all? {|i| i.custom_field_value(2).present?}
219 241 end
220 242
221 243 def test_numeric_filter_should_not_accept_non_numeric_values
222 244 query = IssueQuery.new(:name => '_')
223 245 query.add_filter('estimated_hours', '=', ['a'])
224 246
225 247 assert query.has_filter?('estimated_hours')
226 248 assert !query.valid?
227 249 end
228 250
229 251 def test_operator_is_on_float
230 252 Issue.where(:id => 2).update_all("estimated_hours = 171.2")
231 253 query = IssueQuery.new(:name => '_')
232 254 query.add_filter('estimated_hours', '=', ['171.20'])
233 255 issues = find_issues_with_query(query)
234 256 assert_equal 1, issues.size
235 257 assert_equal 2, issues.first.id
236 258 end
237 259
238 260 def test_operator_is_on_issue_id_should_accept_comma_separated_values
239 261 query = IssueQuery.new(:name => '_')
240 262 query.add_filter("issue_id", '=', ['1,3'])
241 263 issues = find_issues_with_query(query)
242 264 assert_equal 2, issues.size
243 265 assert_equal [1,3], issues.map(&:id).sort
244 266 end
245 267
246 268 def test_operator_between_on_issue_id_should_return_range
247 269 query = IssueQuery.new(:name => '_')
248 270 query.add_filter("issue_id", '><', ['2','3'])
249 271 issues = find_issues_with_query(query)
250 272 assert_equal 2, issues.size
251 273 assert_equal [2,3], issues.map(&:id).sort
252 274 end
253 275
254 276 def test_operator_is_on_integer_custom_field
255 277 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
256 278 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
257 279 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
258 280 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
259 281
260 282 query = IssueQuery.new(:name => '_')
261 283 query.add_filter("cf_#{f.id}", '=', ['12'])
262 284 issues = find_issues_with_query(query)
263 285 assert_equal 1, issues.size
264 286 assert_equal 2, issues.first.id
265 287 end
266 288
267 289 def test_operator_is_on_integer_custom_field_should_accept_negative_value
268 290 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
269 291 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
270 292 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
271 293 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
272 294
273 295 query = IssueQuery.new(:name => '_')
274 296 query.add_filter("cf_#{f.id}", '=', ['-12'])
275 297 assert query.valid?
276 298 issues = find_issues_with_query(query)
277 299 assert_equal 1, issues.size
278 300 assert_equal 2, issues.first.id
279 301 end
280 302
281 303 def test_operator_is_on_float_custom_field
282 304 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
283 305 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
284 306 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
285 307 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
286 308
287 309 query = IssueQuery.new(:name => '_')
288 310 query.add_filter("cf_#{f.id}", '=', ['12.7'])
289 311 issues = find_issues_with_query(query)
290 312 assert_equal 1, issues.size
291 313 assert_equal 2, issues.first.id
292 314 end
293 315
294 316 def test_operator_is_on_float_custom_field_should_accept_negative_value
295 317 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
296 318 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
297 319 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
298 320 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
299 321
300 322 query = IssueQuery.new(:name => '_')
301 323 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
302 324 assert query.valid?
303 325 issues = find_issues_with_query(query)
304 326 assert_equal 1, issues.size
305 327 assert_equal 2, issues.first.id
306 328 end
307 329
308 330 def test_operator_is_on_multi_list_custom_field
309 331 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
310 332 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
311 333 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
312 334 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
313 335 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
314 336
315 337 query = IssueQuery.new(:name => '_')
316 338 query.add_filter("cf_#{f.id}", '=', ['value1'])
317 339 issues = find_issues_with_query(query)
318 340 assert_equal [1, 3], issues.map(&:id).sort
319 341
320 342 query = IssueQuery.new(:name => '_')
321 343 query.add_filter("cf_#{f.id}", '=', ['value2'])
322 344 issues = find_issues_with_query(query)
323 345 assert_equal [1], issues.map(&:id).sort
324 346 end
325 347
326 348 def test_operator_is_not_on_multi_list_custom_field
327 349 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
328 350 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
329 351 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
330 352 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
331 353 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
332 354
333 355 query = IssueQuery.new(:name => '_')
334 356 query.add_filter("cf_#{f.id}", '!', ['value1'])
335 357 issues = find_issues_with_query(query)
336 358 assert !issues.map(&:id).include?(1)
337 359 assert !issues.map(&:id).include?(3)
338 360
339 361 query = IssueQuery.new(:name => '_')
340 362 query.add_filter("cf_#{f.id}", '!', ['value2'])
341 363 issues = find_issues_with_query(query)
342 364 assert !issues.map(&:id).include?(1)
343 365 assert issues.map(&:id).include?(3)
344 366 end
345 367
346 368 def test_operator_is_on_string_custom_field_with_utf8_value
347 369 f = IssueCustomField.create!(:name => 'filter', :field_format => 'string', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
348 370 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'KiÑ»ƒm')
349 371
350 372 query = IssueQuery.new(:name => '_')
351 373 query.add_filter("cf_#{f.id}", '=', ['KiÑ»ƒm'])
352 374 issues = find_issues_with_query(query)
353 375 assert_equal [1], issues.map(&:id).sort
354 376 end
355 377
356 378 def test_operator_is_on_is_private_field
357 379 # is_private filter only available for those who can set issues private
358 380 User.current = User.find(2)
359 381
360 382 query = IssueQuery.new(:name => '_')
361 383 assert query.available_filters.key?('is_private')
362 384
363 385 query.add_filter("is_private", '=', ['1'])
364 386 issues = find_issues_with_query(query)
365 387 assert issues.any?
366 388 assert_nil issues.detect {|issue| !issue.is_private?}
367 389 ensure
368 390 User.current = nil
369 391 end
370 392
371 393 def test_operator_is_not_on_is_private_field
372 394 # is_private filter only available for those who can set issues private
373 395 User.current = User.find(2)
374 396
375 397 query = IssueQuery.new(:name => '_')
376 398 assert query.available_filters.key?('is_private')
377 399
378 400 query.add_filter("is_private", '!', ['1'])
379 401 issues = find_issues_with_query(query)
380 402 assert issues.any?
381 403 assert_nil issues.detect {|issue| issue.is_private?}
382 404 ensure
383 405 User.current = nil
384 406 end
385 407
386 408 def test_operator_greater_than
387 409 query = IssueQuery.new(:project => Project.find(1), :name => '_')
388 410 query.add_filter('done_ratio', '>=', ['40'])
389 411 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
390 412 find_issues_with_query(query)
391 413 end
392 414
393 415 def test_operator_greater_than_a_float
394 416 query = IssueQuery.new(:project => Project.find(1), :name => '_')
395 417 query.add_filter('estimated_hours', '>=', ['40.5'])
396 418 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
397 419 find_issues_with_query(query)
398 420 end
399 421
400 422 def test_operator_greater_than_on_int_custom_field
401 423 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
402 424 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
403 425 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
404 426 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
405 427
406 428 query = IssueQuery.new(:project => Project.find(1), :name => '_')
407 429 query.add_filter("cf_#{f.id}", '>=', ['8'])
408 430 issues = find_issues_with_query(query)
409 431 assert_equal 1, issues.size
410 432 assert_equal 2, issues.first.id
411 433 end
412 434
413 435 def test_operator_lesser_than
414 436 query = IssueQuery.new(:project => Project.find(1), :name => '_')
415 437 query.add_filter('done_ratio', '<=', ['30'])
416 438 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
417 439 find_issues_with_query(query)
418 440 end
419 441
420 442 def test_operator_lesser_than_on_custom_field
421 443 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
422 444 query = IssueQuery.new(:project => Project.find(1), :name => '_')
423 445 query.add_filter("cf_#{f.id}", '<=', ['30'])
424 446 assert_match /CAST.+ <= 30\.0/, query.statement
425 447 find_issues_with_query(query)
426 448 end
427 449
428 450 def test_operator_lesser_than_on_date_custom_field
429 451 f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
430 452 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11')
431 453 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14')
432 454 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
433 455
434 456 query = IssueQuery.new(:project => Project.find(1), :name => '_')
435 457 query.add_filter("cf_#{f.id}", '<=', ['2013-05-01'])
436 458 issue_ids = find_issues_with_query(query).map(&:id)
437 459 assert_include 1, issue_ids
438 460 assert_not_include 2, issue_ids
439 461 assert_not_include 3, issue_ids
440 462 end
441 463
442 464 def test_operator_between
443 465 query = IssueQuery.new(:project => Project.find(1), :name => '_')
444 466 query.add_filter('done_ratio', '><', ['30', '40'])
445 467 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
446 468 find_issues_with_query(query)
447 469 end
448 470
449 471 def test_operator_between_on_custom_field
450 472 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
451 473 query = IssueQuery.new(:project => Project.find(1), :name => '_')
452 474 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
453 475 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
454 476 find_issues_with_query(query)
455 477 end
456 478
457 479 def test_date_filter_should_not_accept_non_date_values
458 480 query = IssueQuery.new(:name => '_')
459 481 query.add_filter('created_on', '=', ['a'])
460 482
461 483 assert query.has_filter?('created_on')
462 484 assert !query.valid?
463 485 end
464 486
465 487 def test_date_filter_should_not_accept_invalid_date_values
466 488 query = IssueQuery.new(:name => '_')
467 489 query.add_filter('created_on', '=', ['2011-01-34'])
468 490
469 491 assert query.has_filter?('created_on')
470 492 assert !query.valid?
471 493 end
472 494
473 495 def test_relative_date_filter_should_not_accept_non_integer_values
474 496 query = IssueQuery.new(:name => '_')
475 497 query.add_filter('created_on', '>t-', ['a'])
476 498
477 499 assert query.has_filter?('created_on')
478 500 assert !query.valid?
479 501 end
480 502
481 503 def test_operator_date_equals
482 504 query = IssueQuery.new(:name => '_')
483 505 query.add_filter('due_date', '=', ['2011-07-10'])
484 506 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/,
485 507 query.statement
486 508 find_issues_with_query(query)
487 509 end
488 510
489 511 def test_operator_date_lesser_than
490 512 query = IssueQuery.new(:name => '_')
491 513 query.add_filter('due_date', '<=', ['2011-07-10'])
492 514 assert_match /issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/, query.statement
493 515 find_issues_with_query(query)
494 516 end
495 517
496 518 def test_operator_date_lesser_than_with_timestamp
497 519 query = IssueQuery.new(:name => '_')
498 520 query.add_filter('updated_on', '<=', ['2011-07-10T19:13:52'])
499 521 assert_match /issues\.updated_on <= '#{quoted_date "2011-07-10"} 19:13:52/, query.statement
500 522 find_issues_with_query(query)
501 523 end
502 524
503 525 def test_operator_date_greater_than
504 526 query = IssueQuery.new(:name => '_')
505 527 query.add_filter('due_date', '>=', ['2011-07-10'])
506 528 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?'/, query.statement
507 529 find_issues_with_query(query)
508 530 end
509 531
510 532 def test_operator_date_greater_than_with_timestamp
511 533 query = IssueQuery.new(:name => '_')
512 534 query.add_filter('updated_on', '>=', ['2011-07-10T19:13:52'])
513 535 assert_match /issues\.updated_on > '#{quoted_date "2011-07-10"} 19:13:51(\.0+)?'/, query.statement
514 536 find_issues_with_query(query)
515 537 end
516 538
517 539 def test_operator_date_between
518 540 query = IssueQuery.new(:name => '_')
519 541 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
520 542 assert_match /issues\.due_date > '#{quoted_date "2011-06-22"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?'/,
521 543 query.statement
522 544 find_issues_with_query(query)
523 545 end
524 546
525 547 def test_operator_in_more_than
526 548 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
527 549 query = IssueQuery.new(:project => Project.find(1), :name => '_')
528 550 query.add_filter('due_date', '>t+', ['15'])
529 551 issues = find_issues_with_query(query)
530 552 assert !issues.empty?
531 553 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
532 554 end
533 555
534 556 def test_operator_in_less_than
535 557 query = IssueQuery.new(:project => Project.find(1), :name => '_')
536 558 query.add_filter('due_date', '<t+', ['15'])
537 559 issues = find_issues_with_query(query)
538 560 assert !issues.empty?
539 561 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
540 562 end
541 563
542 564 def test_operator_in_the_next_days
543 565 query = IssueQuery.new(:project => Project.find(1), :name => '_')
544 566 query.add_filter('due_date', '><t+', ['15'])
545 567 issues = find_issues_with_query(query)
546 568 assert !issues.empty?
547 569 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
548 570 end
549 571
550 572 def test_operator_less_than_ago
551 573 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
552 574 query = IssueQuery.new(:project => Project.find(1), :name => '_')
553 575 query.add_filter('due_date', '>t-', ['3'])
554 576 issues = find_issues_with_query(query)
555 577 assert !issues.empty?
556 578 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
557 579 end
558 580
559 581 def test_operator_in_the_past_days
560 582 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
561 583 query = IssueQuery.new(:project => Project.find(1), :name => '_')
562 584 query.add_filter('due_date', '><t-', ['3'])
563 585 issues = find_issues_with_query(query)
564 586 assert !issues.empty?
565 587 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
566 588 end
567 589
568 590 def test_operator_more_than_ago
569 591 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
570 592 query = IssueQuery.new(:project => Project.find(1), :name => '_')
571 593 query.add_filter('due_date', '<t-', ['10'])
572 594 assert query.statement.include?("#{Issue.table_name}.due_date <=")
573 595 issues = find_issues_with_query(query)
574 596 assert !issues.empty?
575 597 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
576 598 end
577 599
578 600 def test_operator_in
579 601 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
580 602 query = IssueQuery.new(:project => Project.find(1), :name => '_')
581 603 query.add_filter('due_date', 't+', ['2'])
582 604 issues = find_issues_with_query(query)
583 605 assert !issues.empty?
584 606 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
585 607 end
586 608
587 609 def test_operator_ago
588 610 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
589 611 query = IssueQuery.new(:project => Project.find(1), :name => '_')
590 612 query.add_filter('due_date', 't-', ['3'])
591 613 issues = find_issues_with_query(query)
592 614 assert !issues.empty?
593 615 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
594 616 end
595 617
596 618 def test_operator_today
597 619 query = IssueQuery.new(:project => Project.find(1), :name => '_')
598 620 query.add_filter('due_date', 't', [''])
599 621 issues = find_issues_with_query(query)
600 622 assert !issues.empty?
601 623 issues.each {|issue| assert_equal Date.today, issue.due_date}
602 624 end
603 625
604 626 def test_operator_date_periods
605 627 %w(t ld w lw l2w m lm y).each do |operator|
606 628 query = IssueQuery.new(:name => '_')
607 629 query.add_filter('due_date', operator, [''])
608 630 assert query.valid?
609 631 assert query.issues
610 632 end
611 633 end
612 634
613 635 def test_operator_datetime_periods
614 636 %w(t ld w lw l2w m lm y).each do |operator|
615 637 query = IssueQuery.new(:name => '_')
616 638 query.add_filter('created_on', operator, [''])
617 639 assert query.valid?
618 640 assert query.issues
619 641 end
620 642 end
621 643
622 644 def test_operator_contains
623 645 issue = Issue.generate!(:subject => 'AbCdEfG')
624 646
625 647 query = IssueQuery.new(:name => '_')
626 648 query.add_filter('subject', '~', ['cdeF'])
627 649 result = find_issues_with_query(query)
628 650 assert_include issue, result
629 651 result.each {|issue| assert issue.subject.downcase.include?('cdef') }
630 652 end
631 653
632 654 def test_operator_contains_with_utf8_string
633 655 issue = Issue.generate!(:subject => 'Subject contains Kiểm')
634 656
635 657 query = IssueQuery.new(:name => '_')
636 658 query.add_filter('subject', '~', ['Kiểm'])
637 659 result = find_issues_with_query(query)
638 660 assert_include issue, result
639 661 assert_equal 1, result.size
640 662 end
641 663
642 664 def test_operator_does_not_contain
643 665 issue = Issue.generate!(:subject => 'AbCdEfG')
644 666
645 667 query = IssueQuery.new(:name => '_')
646 668 query.add_filter('subject', '!~', ['cdeF'])
647 669 result = find_issues_with_query(query)
648 670 assert_not_include issue, result
649 671 end
650 672
651 673 def test_range_for_this_week_with_week_starting_on_monday
652 674 I18n.locale = :fr
653 675 assert_equal '1', I18n.t(:general_first_day_of_week)
654 676
655 677 Date.stubs(:today).returns(Date.parse('2011-04-29'))
656 678
657 679 query = IssueQuery.new(:project => Project.find(1), :name => '_')
658 680 query.add_filter('due_date', 'w', [''])
659 681 assert_match /issues\.due_date > '#{quoted_date "2011-04-24"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-05-01"} 23:59:59(\.\d+)?/,
660 682 query.statement
661 683 I18n.locale = :en
662 684 end
663 685
664 686 def test_range_for_this_week_with_week_starting_on_sunday
665 687 I18n.locale = :en
666 688 assert_equal '7', I18n.t(:general_first_day_of_week)
667 689
668 690 Date.stubs(:today).returns(Date.parse('2011-04-29'))
669 691
670 692 query = IssueQuery.new(:project => Project.find(1), :name => '_')
671 693 query.add_filter('due_date', 'w', [''])
672 694 assert_match /issues\.due_date > '#{quoted_date "2011-04-23"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-04-30"} 23:59:59(\.\d+)?/,
673 695 query.statement
674 696 end
675 697
676 698 def test_filter_assigned_to_me
677 699 user = User.find(2)
678 700 group = Group.find(10)
679 701 group.users << user
680 702 other_group = Group.find(11)
681 703 Member.create!(:project_id => 1, :principal => group, :role_ids => [1])
682 704 Member.create!(:project_id => 1, :principal => other_group, :role_ids => [1])
683 705 User.current = user
684 706
685 707 with_settings :issue_group_assignment => '1' do
686 708 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
687 709 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
688 710 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => other_group)
689 711
690 712 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
691 713 result = query.issues
692 714 assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
693 715
694 716 assert result.include?(i1)
695 717 assert result.include?(i2)
696 718 assert !result.include?(i3)
697 719 end
698 720 end
699 721
700 722 def test_user_custom_field_filtered_on_me
701 723 User.current = User.find(2)
702 724 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
703 725 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
704 726 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
705 727
706 728 query = IssueQuery.new(:name => '_', :project => Project.find(1))
707 729 filter = query.available_filters["cf_#{cf.id}"]
708 730 assert_not_nil filter
709 731 assert_include 'me', filter[:values].map{|v| v[1]}
710 732
711 733 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
712 734 result = query.issues
713 735 assert_equal 1, result.size
714 736 assert_equal issue1, result.first
715 737 end
716 738
717 739 def test_filter_on_me_by_anonymous_user
718 740 User.current = nil
719 741 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
720 742 assert_equal [], query.issues
721 743 end
722 744
723 745 def test_filter_my_projects
724 746 User.current = User.find(2)
725 747 query = IssueQuery.new(:name => '_')
726 748 filter = query.available_filters['project_id']
727 749 assert_not_nil filter
728 750 assert_include 'mine', filter[:values].map{|v| v[1]}
729 751
730 752 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
731 753 result = query.issues
732 754 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
733 755 end
734 756
735 757 def test_filter_watched_issues
736 758 User.current = User.find(1)
737 759 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
738 760 result = find_issues_with_query(query)
739 761 assert_not_nil result
740 762 assert !result.empty?
741 763 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
742 764 User.current = nil
743 765 end
744 766
745 767 def test_filter_unwatched_issues
746 768 User.current = User.find(1)
747 769 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
748 770 result = find_issues_with_query(query)
749 771 assert_not_nil result
750 772 assert !result.empty?
751 773 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
752 774 User.current = nil
753 775 end
754 776
755 777 def test_filter_on_custom_field_should_ignore_projects_with_field_disabled
756 778 field = IssueCustomField.generate!(:trackers => Tracker.all, :project_ids => [1, 3, 4], :is_for_all => false, :is_filter => true)
757 779 Issue.generate!(:project_id => 3, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
758 780 Issue.generate!(:project_id => 4, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
759 781
760 782 query = IssueQuery.new(:name => '_', :project => Project.find(1))
761 783 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
762 784 assert_equal 2, find_issues_with_query(query).size
763 785
764 786 field.project_ids = [1, 3] # Disable the field for project 4
765 787 field.save!
766 788 assert_equal 1, find_issues_with_query(query).size
767 789 end
768 790
769 791 def test_filter_on_custom_field_should_ignore_trackers_with_field_disabled
770 792 field = IssueCustomField.generate!(:tracker_ids => [1, 2], :is_for_all => true, :is_filter => true)
771 793 Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => 'Foo'})
772 794 Issue.generate!(:project_id => 1, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
773 795
774 796 query = IssueQuery.new(:name => '_', :project => Project.find(1))
775 797 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
776 798 assert_equal 2, find_issues_with_query(query).size
777 799
778 800 field.tracker_ids = [1] # Disable the field for tracker 2
779 801 field.save!
780 802 assert_equal 1, find_issues_with_query(query).size
781 803 end
782 804
783 805 def test_filter_on_project_custom_field
784 806 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
785 807 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
786 808 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
787 809
788 810 query = IssueQuery.new(:name => '_')
789 811 filter_name = "project.cf_#{field.id}"
790 812 assert_include filter_name, query.available_filters.keys
791 813 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
792 814 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
793 815 end
794 816
795 817 def test_filter_on_author_custom_field
796 818 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
797 819 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
798 820
799 821 query = IssueQuery.new(:name => '_')
800 822 filter_name = "author.cf_#{field.id}"
801 823 assert_include filter_name, query.available_filters.keys
802 824 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
803 825 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
804 826 end
805 827
806 828 def test_filter_on_assigned_to_custom_field
807 829 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
808 830 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
809 831
810 832 query = IssueQuery.new(:name => '_')
811 833 filter_name = "assigned_to.cf_#{field.id}"
812 834 assert_include filter_name, query.available_filters.keys
813 835 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
814 836 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
815 837 end
816 838
817 839 def test_filter_on_fixed_version_custom_field
818 840 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
819 841 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
820 842
821 843 query = IssueQuery.new(:name => '_')
822 844 filter_name = "fixed_version.cf_#{field.id}"
823 845 assert_include filter_name, query.available_filters.keys
824 846 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
825 847 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
826 848 end
827 849
828 850 def test_filter_on_fixed_version_due_date
829 851 query = IssueQuery.new(:name => '_')
830 852 filter_name = "fixed_version.due_date"
831 853 assert_include filter_name, query.available_filters.keys
832 854 query.filters = {filter_name => {:operator => '=', :values => [20.day.from_now.to_date.to_s(:db)]}}
833 855 issues = find_issues_with_query(query)
834 856 assert_equal [2], issues.map(&:fixed_version_id).uniq.sort
835 857 assert_equal [2, 12], issues.map(&:id).sort
836 858
837 859 query = IssueQuery.new(:name => '_')
838 860 query.filters = {filter_name => {:operator => '>=', :values => [21.day.from_now.to_date.to_s(:db)]}}
839 861 assert_equal 0, find_issues_with_query(query).size
840 862 end
841 863
842 864 def test_filter_on_fixed_version_status
843 865 query = IssueQuery.new(:name => '_')
844 866 filter_name = "fixed_version.status"
845 867 assert_include filter_name, query.available_filters.keys
846 868 query.filters = {filter_name => {:operator => '=', :values => ['closed']}}
847 869 issues = find_issues_with_query(query)
848 870
849 871 assert_equal [1], issues.map(&:fixed_version_id).sort
850 872 assert_equal [11], issues.map(&:id).sort
851 873
852 874 # "is not" operator should include issues without target version
853 875 query = IssueQuery.new(:name => '_')
854 876 query.filters = {filter_name => {:operator => '!', :values => ['open', 'closed', 'locked']}, "project_id" => {:operator => '=', :values => [1]}}
855 877 assert_equal [1, 3, 7, 8], find_issues_with_query(query).map(&:id).uniq.sort
856 878 end
857 879
858 880 def test_filter_on_relations_with_a_specific_issue
859 881 IssueRelation.delete_all
860 882 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
861 883 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
862 884
863 885 query = IssueQuery.new(:name => '_')
864 886 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
865 887 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
866 888
867 889 query = IssueQuery.new(:name => '_')
868 890 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
869 891 assert_equal [1], find_issues_with_query(query).map(&:id).sort
870 892 end
871 893
872 894 def test_filter_on_relations_with_any_issues_in_a_project
873 895 IssueRelation.delete_all
874 896 with_settings :cross_project_issue_relations => '1' do
875 897 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
876 898 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
877 899 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
878 900 end
879 901
880 902 query = IssueQuery.new(:name => '_')
881 903 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
882 904 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
883 905
884 906 query = IssueQuery.new(:name => '_')
885 907 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
886 908 assert_equal [1], find_issues_with_query(query).map(&:id).sort
887 909
888 910 query = IssueQuery.new(:name => '_')
889 911 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
890 912 assert_equal [], find_issues_with_query(query).map(&:id).sort
891 913 end
892 914
893 915 def test_filter_on_relations_with_any_issues_not_in_a_project
894 916 IssueRelation.delete_all
895 917 with_settings :cross_project_issue_relations => '1' do
896 918 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
897 919 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
898 920 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
899 921 end
900 922
901 923 query = IssueQuery.new(:name => '_')
902 924 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
903 925 assert_equal [1], find_issues_with_query(query).map(&:id).sort
904 926 end
905 927
906 928 def test_filter_on_relations_with_no_issues_in_a_project
907 929 IssueRelation.delete_all
908 930 with_settings :cross_project_issue_relations => '1' do
909 931 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
910 932 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
911 933 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
912 934 end
913 935
914 936 query = IssueQuery.new(:name => '_')
915 937 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
916 938 ids = find_issues_with_query(query).map(&:id).sort
917 939 assert_include 2, ids
918 940 assert_not_include 1, ids
919 941 assert_not_include 3, ids
920 942 end
921 943
922 944 def test_filter_on_relations_with_any_open_issues
923 945 IssueRelation.delete_all
924 946 # Issue 1 is blocked by 8, which is closed
925 947 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(1), :issue_to => Issue.find(8))
926 948 # Issue 2 is blocked by 3, which is open
927 949 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(2), :issue_to => Issue.find(3))
928 950
929 951 query = IssueQuery.new(:name => '_')
930 952 query.filters = {"blocked" => {:operator => "*o", :values => ['']}}
931 953 ids = find_issues_with_query(query).map(&:id)
932 954 assert_equal [], ids & [1]
933 955 assert_include 2, ids
934 956 end
935 957
936 958 def test_filter_on_relations_with_no_open_issues
937 959 IssueRelation.delete_all
938 960 # Issue 1 is blocked by 8, which is closed
939 961 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(1), :issue_to => Issue.find(8))
940 962 # Issue 2 is blocked by 3, which is open
941 963 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(2), :issue_to => Issue.find(3))
942 964
943 965 query = IssueQuery.new(:name => '_')
944 966 query.filters = {"blocked" => {:operator => "!o", :values => ['']}}
945 967 ids = find_issues_with_query(query).map(&:id)
946 968 assert_equal [], ids & [2]
947 969 assert_include 1, ids
948 970 end
949 971
950 972 def test_filter_on_relations_with_no_issues
951 973 IssueRelation.delete_all
952 974 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
953 975 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
954 976
955 977 query = IssueQuery.new(:name => '_')
956 978 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
957 979 ids = find_issues_with_query(query).map(&:id)
958 980 assert_equal [], ids & [1, 2, 3]
959 981 assert_include 4, ids
960 982 end
961 983
962 984 def test_filter_on_relations_with_any_issues
963 985 IssueRelation.delete_all
964 986 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
965 987 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
966 988
967 989 query = IssueQuery.new(:name => '_')
968 990 query.filters = {"relates" => {:operator => '*', :values => ['']}}
969 991 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
970 992 end
971 993
972 994 def test_filter_on_relations_should_not_ignore_other_filter
973 995 issue = Issue.generate!
974 996 issue1 = Issue.generate!(:status_id => 1)
975 997 issue2 = Issue.generate!(:status_id => 2)
976 998 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue1)
977 999 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue2)
978 1000
979 1001 query = IssueQuery.new(:name => '_')
980 1002 query.filters = {
981 1003 "status_id" => {:operator => '=', :values => ['1']},
982 1004 "relates" => {:operator => '=', :values => [issue.id.to_s]}
983 1005 }
984 1006 assert_equal [issue1], find_issues_with_query(query)
985 1007 end
986 1008
987 1009 def test_filter_on_parent
988 1010 Issue.delete_all
989 1011 parent = Issue.generate_with_descendants!
990 1012
991 1013
992 1014 query = IssueQuery.new(:name => '_')
993 1015 query.filters = {"parent_id" => {:operator => '=', :values => [parent.id.to_s]}}
994 1016 assert_equal parent.children.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
995 1017
996 1018 query.filters = {"parent_id" => {:operator => '~', :values => [parent.id.to_s]}}
997 1019 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
998 1020
999 1021 query.filters = {"parent_id" => {:operator => '*', :values => ['']}}
1000 1022 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1001 1023
1002 1024 query.filters = {"parent_id" => {:operator => '!*', :values => ['']}}
1003 1025 assert_equal [parent.id], find_issues_with_query(query).map(&:id).sort
1004 1026 end
1005 1027
1006 1028 def test_filter_on_invalid_parent_should_return_no_results
1007 1029 query = IssueQuery.new(:name => '_')
1008 1030 query.filters = {"parent_id" => {:operator => '=', :values => '99999999999'}}
1009 1031 assert_equal [], find_issues_with_query(query).map(&:id).sort
1010 1032
1011 1033 query.filters = {"parent_id" => {:operator => '~', :values => '99999999999'}}
1012 1034 assert_equal [], find_issues_with_query(query)
1013 1035 end
1014 1036
1015 1037 def test_filter_on_child
1016 1038 Issue.delete_all
1017 1039 parent = Issue.generate_with_descendants!
1018 1040 child, leaf = parent.children.sort_by(&:id)
1019 1041 grandchild = child.children.first
1020 1042
1021 1043
1022 1044 query = IssueQuery.new(:name => '_')
1023 1045 query.filters = {"child_id" => {:operator => '=', :values => [grandchild.id.to_s]}}
1024 1046 assert_equal [child.id], find_issues_with_query(query).map(&:id).sort
1025 1047
1026 1048 query.filters = {"child_id" => {:operator => '~', :values => [grandchild.id.to_s]}}
1027 1049 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1028 1050
1029 1051 query.filters = {"child_id" => {:operator => '*', :values => ['']}}
1030 1052 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1031 1053
1032 1054 query.filters = {"child_id" => {:operator => '!*', :values => ['']}}
1033 1055 assert_equal [grandchild, leaf].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1034 1056 end
1035 1057
1036 1058 def test_filter_on_invalid_child_should_return_no_results
1037 1059 query = IssueQuery.new(:name => '_')
1038 1060 query.filters = {"child_id" => {:operator => '=', :values => '99999999999'}}
1039 1061 assert_equal [], find_issues_with_query(query)
1040 1062
1041 1063 query.filters = {"child_id" => {:operator => '~', :values => '99999999999'}}
1042 1064 assert_equal [].map(&:id).sort, find_issues_with_query(query)
1043 1065 end
1044 1066
1045 1067 def test_statement_should_be_nil_with_no_filters
1046 1068 q = IssueQuery.new(:name => '_')
1047 1069 q.filters = {}
1048 1070
1049 1071 assert q.valid?
1050 1072 assert_nil q.statement
1051 1073 end
1052 1074
1053 1075 def test_available_filters_as_json_should_include_missing_assigned_to_id_values
1054 1076 user = User.generate!
1055 1077 with_current_user User.find(1) do
1056 1078 q = IssueQuery.new
1057 1079 q.filters = {"assigned_to_id" => {:operator => '=', :values => user.id.to_s}}
1058 1080
1059 1081 filters = q.available_filters_as_json
1060 1082 assert_include [user.name, user.id.to_s], filters['assigned_to_id']['values']
1061 1083 end
1062 1084 end
1063 1085
1064 1086 def test_available_filters_as_json_should_include_missing_author_id_values
1065 1087 user = User.generate!
1066 1088 with_current_user User.find(1) do
1067 1089 q = IssueQuery.new
1068 1090 q.filters = {"author_id" => {:operator => '=', :values => user.id.to_s}}
1069 1091
1070 1092 filters = q.available_filters_as_json
1071 1093 assert_include [user.name, user.id.to_s], filters['author_id']['values']
1072 1094 end
1073 1095 end
1074 1096
1075 1097 def test_default_columns
1076 1098 q = IssueQuery.new
1077 1099 assert q.columns.any?
1078 1100 assert q.inline_columns.any?
1079 1101 assert q.block_columns.empty?
1080 1102 end
1081 1103
1082 1104 def test_set_column_names
1083 1105 q = IssueQuery.new
1084 1106 q.column_names = ['tracker', :subject, '', 'unknonw_column']
1085 1107 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
1086 1108 end
1087 1109
1088 1110 def test_has_column_should_accept_a_column_name
1089 1111 q = IssueQuery.new
1090 1112 q.column_names = ['tracker', :subject]
1091 1113 assert q.has_column?(:tracker)
1092 1114 assert !q.has_column?(:category)
1093 1115 end
1094 1116
1095 1117 def test_has_column_should_accept_a_column
1096 1118 q = IssueQuery.new
1097 1119 q.column_names = ['tracker', :subject]
1098 1120
1099 1121 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
1100 1122 assert_kind_of QueryColumn, tracker_column
1101 1123 category_column = q.available_columns.detect {|c| c.name==:category}
1102 1124 assert_kind_of QueryColumn, category_column
1103 1125
1104 1126 assert q.has_column?(tracker_column)
1105 1127 assert !q.has_column?(category_column)
1106 1128 end
1107 1129
1108 1130 def test_inline_and_block_columns
1109 1131 q = IssueQuery.new
1110 1132 q.column_names = ['subject', 'description', 'tracker']
1111 1133
1112 1134 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
1113 1135 assert_equal [:description], q.block_columns.map(&:name)
1114 1136 end
1115 1137
1116 1138 def test_custom_field_columns_should_be_inline
1117 1139 q = IssueQuery.new
1118 1140 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
1119 1141 assert columns.any?
1120 1142 assert_nil columns.detect {|column| !column.inline?}
1121 1143 end
1122 1144
1123 1145 def test_query_should_preload_spent_hours
1124 1146 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
1125 1147 assert q.has_column?(:spent_hours)
1126 1148 issues = q.issues
1127 1149 assert_not_nil issues.first.instance_variable_get("@spent_hours")
1128 1150 end
1129 1151
1130 1152 def test_groupable_columns_should_include_custom_fields
1131 1153 q = IssueQuery.new
1132 1154 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1133 1155 assert_not_nil column
1134 1156 assert_kind_of QueryCustomFieldColumn, column
1135 1157 end
1136 1158
1137 1159 def test_groupable_columns_should_not_include_multi_custom_fields
1138 1160 field = CustomField.find(1)
1139 1161 field.update_attribute :multiple, true
1140 1162
1141 1163 q = IssueQuery.new
1142 1164 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1143 1165 assert_nil column
1144 1166 end
1145 1167
1146 1168 def test_groupable_columns_should_include_user_custom_fields
1147 1169 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
1148 1170
1149 1171 q = IssueQuery.new
1150 1172 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1151 1173 end
1152 1174
1153 1175 def test_groupable_columns_should_include_version_custom_fields
1154 1176 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
1155 1177
1156 1178 q = IssueQuery.new
1157 1179 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1158 1180 end
1159 1181
1160 1182 def test_grouped_with_valid_column
1161 1183 q = IssueQuery.new(:group_by => 'status')
1162 1184 assert q.grouped?
1163 1185 assert_not_nil q.group_by_column
1164 1186 assert_equal :status, q.group_by_column.name
1165 1187 assert_not_nil q.group_by_statement
1166 1188 assert_equal 'status', q.group_by_statement
1167 1189 end
1168 1190
1169 1191 def test_grouped_with_invalid_column
1170 1192 q = IssueQuery.new(:group_by => 'foo')
1171 1193 assert !q.grouped?
1172 1194 assert_nil q.group_by_column
1173 1195 assert_nil q.group_by_statement
1174 1196 end
1175 1197
1176 1198 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
1177 1199 with_settings :user_format => 'lastname_comma_firstname' do
1178 1200 q = IssueQuery.new
1179 1201 assert q.sortable_columns.has_key?('assigned_to')
1180 1202 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
1181 1203 end
1182 1204 end
1183 1205
1184 1206 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
1185 1207 with_settings :user_format => 'lastname_comma_firstname' do
1186 1208 q = IssueQuery.new
1187 1209 assert q.sortable_columns.has_key?('author')
1188 1210 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
1189 1211 end
1190 1212 end
1191 1213
1192 1214 def test_sortable_columns_should_include_custom_field
1193 1215 q = IssueQuery.new
1194 1216 assert q.sortable_columns['cf_1']
1195 1217 end
1196 1218
1197 1219 def test_sortable_columns_should_not_include_multi_custom_field
1198 1220 field = CustomField.find(1)
1199 1221 field.update_attribute :multiple, true
1200 1222
1201 1223 q = IssueQuery.new
1202 1224 assert !q.sortable_columns['cf_1']
1203 1225 end
1204 1226
1205 1227 def test_default_sort
1206 1228 q = IssueQuery.new
1207 1229 assert_equal [], q.sort_criteria
1208 1230 end
1209 1231
1210 1232 def test_set_sort_criteria_with_hash
1211 1233 q = IssueQuery.new
1212 1234 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
1213 1235 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1214 1236 end
1215 1237
1216 1238 def test_set_sort_criteria_with_array
1217 1239 q = IssueQuery.new
1218 1240 q.sort_criteria = [['priority', 'desc'], 'tracker']
1219 1241 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1220 1242 end
1221 1243
1222 1244 def test_create_query_with_sort
1223 1245 q = IssueQuery.new(:name => 'Sorted')
1224 1246 q.sort_criteria = [['priority', 'desc'], 'tracker']
1225 1247 assert q.save
1226 1248 q.reload
1227 1249 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1228 1250 end
1229 1251
1230 1252 def test_sort_by_string_custom_field_asc
1231 1253 q = IssueQuery.new
1232 1254 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1233 1255 assert c
1234 1256 assert c.sortable
1235 1257 issues = q.issues(:order => "#{c.sortable} ASC")
1236 1258 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1237 1259 assert !values.empty?
1238 1260 assert_equal values.sort, values
1239 1261 end
1240 1262
1241 1263 def test_sort_by_string_custom_field_desc
1242 1264 q = IssueQuery.new
1243 1265 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1244 1266 assert c
1245 1267 assert c.sortable
1246 1268 issues = q.issues(:order => "#{c.sortable} DESC")
1247 1269 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1248 1270 assert !values.empty?
1249 1271 assert_equal values.sort.reverse, values
1250 1272 end
1251 1273
1252 1274 def test_sort_by_float_custom_field_asc
1253 1275 q = IssueQuery.new
1254 1276 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
1255 1277 assert c
1256 1278 assert c.sortable
1257 1279 issues = q.issues(:order => "#{c.sortable} ASC")
1258 1280 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
1259 1281 assert !values.empty?
1260 1282 assert_equal values.sort, values
1261 1283 end
1262 1284
1263 1285 def test_set_totalable_names
1264 1286 q = IssueQuery.new
1265 1287 q.totalable_names = ['estimated_hours', :spent_hours, '']
1266 1288 assert_equal [:estimated_hours, :spent_hours], q.totalable_columns.map(&:name)
1267 1289 end
1268 1290
1269 1291 def test_totalable_columns_should_default_to_settings
1270 1292 with_settings :issue_list_default_totals => ['estimated_hours'] do
1271 1293 q = IssueQuery.new
1272 1294 assert_equal [:estimated_hours], q.totalable_columns.map(&:name)
1273 1295 end
1274 1296 end
1275 1297
1276 1298 def test_available_totalable_columns_should_include_estimated_hours
1277 1299 q = IssueQuery.new
1278 1300 assert_include :estimated_hours, q.available_totalable_columns.map(&:name)
1279 1301 end
1280 1302
1281 1303 def test_available_totalable_columns_should_include_spent_hours
1282 1304 User.current = User.find(1)
1283 1305
1284 1306 q = IssueQuery.new
1285 1307 assert_include :spent_hours, q.available_totalable_columns.map(&:name)
1286 1308 end
1287 1309
1288 1310 def test_available_totalable_columns_should_include_int_custom_field
1289 1311 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1290 1312 q = IssueQuery.new
1291 1313 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1292 1314 end
1293 1315
1294 1316 def test_available_totalable_columns_should_include_float_custom_field
1295 1317 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1296 1318 q = IssueQuery.new
1297 1319 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1298 1320 end
1299 1321
1300 1322 def test_total_for_estimated_hours
1301 1323 Issue.delete_all
1302 1324 Issue.generate!(:estimated_hours => 5.5)
1303 1325 Issue.generate!(:estimated_hours => 1.1)
1304 1326 Issue.generate!
1305 1327
1306 1328 q = IssueQuery.new
1307 1329 assert_equal 6.6, q.total_for(:estimated_hours)
1308 1330 end
1309 1331
1310 1332 def test_total_by_group_for_estimated_hours
1311 1333 Issue.delete_all
1312 1334 Issue.generate!(:estimated_hours => 5.5, :assigned_to_id => 2)
1313 1335 Issue.generate!(:estimated_hours => 1.1, :assigned_to_id => 3)
1314 1336 Issue.generate!(:estimated_hours => 3.5)
1315 1337
1316 1338 q = IssueQuery.new(:group_by => 'assigned_to')
1317 1339 assert_equal(
1318 1340 {nil => 3.5, User.find(2) => 5.5, User.find(3) => 1.1},
1319 1341 q.total_by_group_for(:estimated_hours)
1320 1342 )
1321 1343 end
1322 1344
1323 1345 def test_total_for_spent_hours
1324 1346 TimeEntry.delete_all
1325 1347 TimeEntry.generate!(:hours => 5.5)
1326 1348 TimeEntry.generate!(:hours => 1.1)
1327 1349
1328 1350 q = IssueQuery.new
1329 1351 assert_equal 6.6, q.total_for(:spent_hours)
1330 1352 end
1331 1353
1332 1354 def test_total_by_group_for_spent_hours
1333 1355 TimeEntry.delete_all
1334 1356 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1335 1357 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1336 1358 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1337 1359 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1338 1360
1339 1361 q = IssueQuery.new(:group_by => 'assigned_to')
1340 1362 assert_equal(
1341 1363 {User.find(2) => 5.5, User.find(3) => 1.1},
1342 1364 q.total_by_group_for(:spent_hours)
1343 1365 )
1344 1366 end
1345 1367
1346 1368 def test_total_by_project_group_for_spent_hours
1347 1369 TimeEntry.delete_all
1348 1370 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1349 1371 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1350 1372 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1351 1373 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1352 1374
1353 1375 q = IssueQuery.new(:group_by => 'project')
1354 1376 assert_equal(
1355 1377 {Project.find(1) => 6.6},
1356 1378 q.total_by_group_for(:spent_hours)
1357 1379 )
1358 1380 end
1359 1381
1360 1382 def test_total_for_int_custom_field
1361 1383 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1362 1384 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1363 1385 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1364 1386 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1365 1387
1366 1388 q = IssueQuery.new
1367 1389 assert_equal 9, q.total_for("cf_#{field.id}")
1368 1390 end
1369 1391
1370 1392 def test_total_by_group_for_int_custom_field
1371 1393 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1372 1394 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1373 1395 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1374 1396 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1375 1397 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1376 1398
1377 1399 q = IssueQuery.new(:group_by => 'assigned_to')
1378 1400 assert_equal(
1379 1401 {User.find(2) => 2, User.find(3) => 7},
1380 1402 q.total_by_group_for("cf_#{field.id}")
1381 1403 )
1382 1404 end
1383 1405
1384 1406 def test_total_for_float_custom_field
1385 1407 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1386 1408 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2.3')
1387 1409 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1388 1410 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1389 1411
1390 1412 q = IssueQuery.new
1391 1413 assert_equal 9.3, q.total_for("cf_#{field.id}")
1392 1414 end
1393 1415
1394 1416 def test_invalid_query_should_raise_query_statement_invalid_error
1395 1417 q = IssueQuery.new
1396 1418 assert_raise Query::StatementInvalid do
1397 1419 q.issues(:conditions => "foo = 1")
1398 1420 end
1399 1421 end
1400 1422
1401 1423 def test_issue_count
1402 1424 q = IssueQuery.new(:name => '_')
1403 1425 issue_count = q.issue_count
1404 1426 assert_equal q.issues.size, issue_count
1405 1427 end
1406 1428
1407 1429 def test_issue_count_with_archived_issues
1408 1430 p = Project.generate! do |project|
1409 1431 project.status = Project::STATUS_ARCHIVED
1410 1432 end
1411 1433 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
1412 1434 assert !i.visible?
1413 1435
1414 1436 test_issue_count
1415 1437 end
1416 1438
1417 1439 def test_issue_count_by_association_group
1418 1440 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1419 1441 count_by_group = q.issue_count_by_group
1420 1442 assert_kind_of Hash, count_by_group
1421 1443 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1422 1444 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1423 1445 assert count_by_group.has_key?(User.find(3))
1424 1446 end
1425 1447
1426 1448 def test_issue_count_by_list_custom_field_group
1427 1449 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
1428 1450 count_by_group = q.issue_count_by_group
1429 1451 assert_kind_of Hash, count_by_group
1430 1452 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1431 1453 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1432 1454 assert count_by_group.has_key?('MySQL')
1433 1455 end
1434 1456
1435 1457 def test_issue_count_by_date_custom_field_group
1436 1458 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
1437 1459 count_by_group = q.issue_count_by_group
1438 1460 assert_kind_of Hash, count_by_group
1439 1461 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1440 1462 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1441 1463 end
1442 1464
1443 1465 def test_issue_count_with_nil_group_only
1444 1466 Issue.update_all("assigned_to_id = NULL")
1445 1467
1446 1468 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1447 1469 count_by_group = q.issue_count_by_group
1448 1470 assert_kind_of Hash, count_by_group
1449 1471 assert_equal 1, count_by_group.keys.size
1450 1472 assert_nil count_by_group.keys.first
1451 1473 end
1452 1474
1453 1475 def test_issue_ids
1454 1476 q = IssueQuery.new(:name => '_')
1455 1477 order = "issues.subject, issues.id"
1456 1478 issues = q.issues(:order => order)
1457 1479 assert_equal issues.map(&:id), q.issue_ids(:order => order)
1458 1480 end
1459 1481
1460 1482 def test_label_for
1461 1483 set_language_if_valid 'en'
1462 1484 q = IssueQuery.new
1463 1485 assert_equal 'Assignee', q.label_for('assigned_to_id')
1464 1486 end
1465 1487
1466 1488 def test_label_for_fr
1467 1489 set_language_if_valid 'fr'
1468 1490 q = IssueQuery.new
1469 1491 assert_equal "Assign\xc3\xa9 \xc3\xa0".force_encoding('UTF-8'), q.label_for('assigned_to_id')
1470 1492 end
1471 1493
1472 1494 def test_editable_by
1473 1495 admin = User.find(1)
1474 1496 manager = User.find(2)
1475 1497 developer = User.find(3)
1476 1498
1477 1499 # Public query on project 1
1478 1500 q = IssueQuery.find(1)
1479 1501 assert q.editable_by?(admin)
1480 1502 assert q.editable_by?(manager)
1481 1503 assert !q.editable_by?(developer)
1482 1504
1483 1505 # Private query on project 1
1484 1506 q = IssueQuery.find(2)
1485 1507 assert q.editable_by?(admin)
1486 1508 assert !q.editable_by?(manager)
1487 1509 assert q.editable_by?(developer)
1488 1510
1489 1511 # Private query for all projects
1490 1512 q = IssueQuery.find(3)
1491 1513 assert q.editable_by?(admin)
1492 1514 assert !q.editable_by?(manager)
1493 1515 assert q.editable_by?(developer)
1494 1516
1495 1517 # Public query for all projects
1496 1518 q = IssueQuery.find(4)
1497 1519 assert q.editable_by?(admin)
1498 1520 assert !q.editable_by?(manager)
1499 1521 assert !q.editable_by?(developer)
1500 1522 end
1501 1523
1502 1524 def test_visible_scope
1503 1525 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1504 1526
1505 1527 assert query_ids.include?(1), 'public query on public project was not visible'
1506 1528 assert query_ids.include?(4), 'public query for all projects was not visible'
1507 1529 assert !query_ids.include?(2), 'private query on public project was visible'
1508 1530 assert !query_ids.include?(3), 'private query for all projects was visible'
1509 1531 assert !query_ids.include?(7), 'public query on private project was visible'
1510 1532 end
1511 1533
1512 1534 def test_query_with_public_visibility_should_be_visible_to_anyone
1513 1535 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC)
1514 1536
1515 1537 assert q.visible?(User.anonymous)
1516 1538 assert IssueQuery.visible(User.anonymous).find_by_id(q.id)
1517 1539
1518 1540 assert q.visible?(User.find(7))
1519 1541 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1520 1542
1521 1543 assert q.visible?(User.find(2))
1522 1544 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1523 1545
1524 1546 assert q.visible?(User.find(1))
1525 1547 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1526 1548 end
1527 1549
1528 1550 def test_query_with_roles_visibility_should_be_visible_to_user_with_role
1529 1551 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2])
1530 1552
1531 1553 assert !q.visible?(User.anonymous)
1532 1554 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1533 1555
1534 1556 assert !q.visible?(User.find(7))
1535 1557 assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id)
1536 1558
1537 1559 assert q.visible?(User.find(2))
1538 1560 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1539 1561
1540 1562 assert q.visible?(User.find(1))
1541 1563 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1542 1564 end
1543 1565
1544 1566 def test_query_with_private_visibility_should_be_visible_to_owner
1545 1567 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7))
1546 1568
1547 1569 assert !q.visible?(User.anonymous)
1548 1570 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1549 1571
1550 1572 assert q.visible?(User.find(7))
1551 1573 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1552 1574
1553 1575 assert !q.visible?(User.find(2))
1554 1576 assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id)
1555 1577
1556 1578 assert q.visible?(User.find(1))
1557 1579 assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id)
1558 1580 end
1559 1581
1560 1582 test "#available_filters should include users of visible projects in cross-project view" do
1561 1583 users = IssueQuery.new.available_filters["assigned_to_id"]
1562 1584 assert_not_nil users
1563 1585 assert users[:values].map{|u|u[1]}.include?("3")
1564 1586 end
1565 1587
1566 1588 test "#available_filters should include users of subprojects" do
1567 1589 user1 = User.generate!
1568 1590 user2 = User.generate!
1569 1591 project = Project.find(1)
1570 1592 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1571 1593
1572 1594 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1573 1595 assert_not_nil users
1574 1596 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1575 1597 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1576 1598 end
1577 1599
1578 1600 test "#available_filters should include visible projects in cross-project view" do
1579 1601 projects = IssueQuery.new.available_filters["project_id"]
1580 1602 assert_not_nil projects
1581 1603 assert projects[:values].map{|u|u[1]}.include?("1")
1582 1604 end
1583 1605
1584 1606 test "#available_filters should include 'member_of_group' filter" do
1585 1607 query = IssueQuery.new
1586 1608 assert query.available_filters.keys.include?("member_of_group")
1587 1609 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1588 1610 assert query.available_filters["member_of_group"][:values].present?
1589 1611 assert_equal Group.givable.sort.map {|g| [g.name, g.id.to_s]},
1590 1612 query.available_filters["member_of_group"][:values].sort
1591 1613 end
1592 1614
1593 1615 test "#available_filters should include 'assigned_to_role' filter" do
1594 1616 query = IssueQuery.new
1595 1617 assert query.available_filters.keys.include?("assigned_to_role")
1596 1618 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1597 1619
1598 1620 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1599 1621 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1600 1622 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1601 1623
1602 1624 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1603 1625 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1604 1626 end
1605 1627
1606 1628 def test_available_filters_should_include_custom_field_according_to_user_visibility
1607 1629 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1608 1630 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1609 1631
1610 1632 with_current_user User.find(3) do
1611 1633 query = IssueQuery.new
1612 1634 assert_include "cf_#{visible_field.id}", query.available_filters.keys
1613 1635 assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys
1614 1636 end
1615 1637 end
1616 1638
1617 1639 def test_available_columns_should_include_custom_field_according_to_user_visibility
1618 1640 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1619 1641 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1620 1642
1621 1643 with_current_user User.find(3) do
1622 1644 query = IssueQuery.new
1623 1645 assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name)
1624 1646 assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name)
1625 1647 end
1626 1648 end
1627 1649
1628 1650 def setup_member_of_group
1629 1651 Group.destroy_all # No fixtures
1630 1652 @user_in_group = User.generate!
1631 1653 @second_user_in_group = User.generate!
1632 1654 @user_in_group2 = User.generate!
1633 1655 @user_not_in_group = User.generate!
1634 1656
1635 1657 @group = Group.generate!.reload
1636 1658 @group.users << @user_in_group
1637 1659 @group.users << @second_user_in_group
1638 1660
1639 1661 @group2 = Group.generate!.reload
1640 1662 @group2.users << @user_in_group2
1641 1663
1642 1664 @query = IssueQuery.new(:name => '_')
1643 1665 end
1644 1666
1645 1667 test "member_of_group filter should search assigned to for users in the group" do
1646 1668 setup_member_of_group
1647 1669 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1648 1670
1649 1671 assert_find_issues_with_query_is_successful @query
1650 1672 end
1651 1673
1652 1674 test "member_of_group filter should search not assigned to any group member (none)" do
1653 1675 setup_member_of_group
1654 1676 @query.add_filter('member_of_group', '!*', [''])
1655 1677
1656 1678 assert_find_issues_with_query_is_successful @query
1657 1679 end
1658 1680
1659 1681 test "member_of_group filter should search assigned to any group member (all)" do
1660 1682 setup_member_of_group
1661 1683 @query.add_filter('member_of_group', '*', [''])
1662 1684
1663 1685 assert_find_issues_with_query_is_successful @query
1664 1686 end
1665 1687
1666 1688 test "member_of_group filter should return an empty set with = empty group" do
1667 1689 setup_member_of_group
1668 1690 @empty_group = Group.generate!
1669 1691 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1670 1692
1671 1693 assert_equal [], find_issues_with_query(@query)
1672 1694 end
1673 1695
1674 1696 test "member_of_group filter should return issues with ! empty group" do
1675 1697 setup_member_of_group
1676 1698 @empty_group = Group.generate!
1677 1699 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1678 1700
1679 1701 assert_find_issues_with_query_is_successful @query
1680 1702 end
1681 1703
1682 1704 def setup_assigned_to_role
1683 1705 @manager_role = Role.find_by_name('Manager')
1684 1706 @developer_role = Role.find_by_name('Developer')
1685 1707
1686 1708 @project = Project.generate!
1687 1709 @manager = User.generate!
1688 1710 @developer = User.generate!
1689 1711 @boss = User.generate!
1690 1712 @guest = User.generate!
1691 1713 User.add_to_project(@manager, @project, @manager_role)
1692 1714 User.add_to_project(@developer, @project, @developer_role)
1693 1715 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1694 1716
1695 1717 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1696 1718 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1697 1719 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1698 1720 @issue4 = Issue.generate!(:project => @project, :author_id => @guest.id, :assigned_to_id => @guest.id)
1699 1721 @issue5 = Issue.generate!(:project => @project)
1700 1722
1701 1723 @query = IssueQuery.new(:name => '_', :project => @project)
1702 1724 end
1703 1725
1704 1726 test "assigned_to_role filter should search assigned to for users with the Role" do
1705 1727 setup_assigned_to_role
1706 1728 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1707 1729
1708 1730 assert_query_result [@issue1, @issue3], @query
1709 1731 end
1710 1732
1711 1733 test "assigned_to_role filter should search assigned to for users with the Role on the issue project" do
1712 1734 setup_assigned_to_role
1713 1735 other_project = Project.generate!
1714 1736 User.add_to_project(@developer, other_project, @manager_role)
1715 1737 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1716 1738
1717 1739 assert_query_result [@issue1, @issue3], @query
1718 1740 end
1719 1741
1720 1742 test "assigned_to_role filter should return an empty set with empty role" do
1721 1743 setup_assigned_to_role
1722 1744 @empty_role = Role.generate!
1723 1745 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1724 1746
1725 1747 assert_query_result [], @query
1726 1748 end
1727 1749
1728 1750 test "assigned_to_role filter should search assigned to for users without the Role" do
1729 1751 setup_assigned_to_role
1730 1752 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1731 1753
1732 1754 assert_query_result [@issue2, @issue4, @issue5], @query
1733 1755 end
1734 1756
1735 1757 test "assigned_to_role filter should search assigned to for users not assigned to any Role (none)" do
1736 1758 setup_assigned_to_role
1737 1759 @query.add_filter('assigned_to_role', '!*', [''])
1738 1760
1739 1761 assert_query_result [@issue4, @issue5], @query
1740 1762 end
1741 1763
1742 1764 test "assigned_to_role filter should search assigned to for users assigned to any Role (all)" do
1743 1765 setup_assigned_to_role
1744 1766 @query.add_filter('assigned_to_role', '*', [''])
1745 1767
1746 1768 assert_query_result [@issue1, @issue2, @issue3], @query
1747 1769 end
1748 1770
1749 1771 test "assigned_to_role filter should return issues with ! empty role" do
1750 1772 setup_assigned_to_role
1751 1773 @empty_role = Role.generate!
1752 1774 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1753 1775
1754 1776 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1755 1777 end
1756 1778
1757 1779 def test_query_column_should_accept_a_symbol_as_caption
1758 1780 set_language_if_valid 'en'
1759 1781 c = QueryColumn.new('foo', :caption => :general_text_Yes)
1760 1782 assert_equal 'Yes', c.caption
1761 1783 end
1762 1784
1763 1785 def test_query_column_should_accept_a_proc_as_caption
1764 1786 c = QueryColumn.new('foo', :caption => lambda {'Foo'})
1765 1787 assert_equal 'Foo', c.caption
1766 1788 end
1767 1789
1768 1790 def test_date_clause_should_respect_user_time_zone_with_local_default
1769 1791 @query = IssueQuery.new(:name => '_')
1770 1792
1771 1793 # user is in Hawaii (-10)
1772 1794 User.current = users(:users_001)
1773 1795 User.current.pref.update_attribute :time_zone, 'Hawaii'
1774 1796
1775 1797 # assume timestamps are stored in server local time
1776 1798 local_zone = Time.zone
1777 1799
1778 1800 from = Date.parse '2016-03-20'
1779 1801 to = Date.parse '2016-03-22'
1780 1802 assert c = @query.send(:date_clause, 'table', 'field', from, to, false)
1781 1803
1782 1804 # the dates should have been interpreted in the user's time zone and
1783 1805 # converted to local time
1784 1806 # what we get exactly in the sql depends on the local time zone, therefore
1785 1807 # it's computed here.
1786 1808 f = User.current.time_zone.local(from.year, from.month, from.day).yesterday.end_of_day.in_time_zone(local_zone)
1787 1809 t = User.current.time_zone.local(to.year, to.month, to.day).end_of_day.in_time_zone(local_zone)
1788 1810 assert_equal "table.field > '#{Query.connection.quoted_date f}' AND table.field <= '#{Query.connection.quoted_date t}'", c
1789 1811 end
1790 1812
1791 1813 def test_date_clause_should_respect_user_time_zone_with_utc_default
1792 1814 @query = IssueQuery.new(:name => '_')
1793 1815
1794 1816 # user is in Hawaii (-10)
1795 1817 User.current = users(:users_001)
1796 1818 User.current.pref.update_attribute :time_zone, 'Hawaii'
1797 1819
1798 1820 # assume timestamps are stored as utc
1799 1821 ActiveRecord::Base.default_timezone = :utc
1800 1822
1801 1823 from = Date.parse '2016-03-20'
1802 1824 to = Date.parse '2016-03-22'
1803 1825 assert c = @query.send(:date_clause, 'table', 'field', from, to, false)
1804 1826 # the dates should have been interpreted in the user's time zone and
1805 1827 # converted to utc. March 20 in Hawaii begins at 10am UTC.
1806 1828 f = Time.new(2016, 3, 20, 9, 59, 59, 0).end_of_hour
1807 1829 t = Time.new(2016, 3, 23, 9, 59, 59, 0).end_of_hour
1808 1830 assert_equal "table.field > '#{Query.connection.quoted_date f}' AND table.field <= '#{Query.connection.quoted_date t}'", c
1809 1831 ensure
1810 1832 ActiveRecord::Base.default_timezone = :local # restore Redmine default
1811 1833 end
1812 1834
1813 1835 end
@@ -1,81 +1,103
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class TimeEntryQueryTest < ActiveSupport::TestCase
21 21 fixtures :issues, :projects, :users,
22 22 :members, :roles, :member_roles,
23 23 :trackers, :issue_statuses,
24 24 :projects_trackers,
25 25 :journals, :journal_details,
26 26 :issue_categories, :enumerations,
27 27 :groups_users,
28 28 :enabled_modules
29 29
30 def test_filter_values_without_project_should_be_arrays
31 q = TimeEntryQuery.new
32 assert_nil q.project
33
34 q.available_filters.each do |name, filter|
35 values = filter.values
36 assert (values.nil? || values.is_a?(Array)),
37 "#values for #{name} filter returned a #{values.class.name}"
38 end
39 end
40
41 def test_filter_values_with_project_should_be_arrays
42 q = TimeEntryQuery.new(:project => Project.find(1))
43 assert_not_nil q.project
44
45 q.available_filters.each do |name, filter|
46 values = filter.values
47 assert (values.nil? || values.is_a?(Array)),
48 "#values for #{name} filter returned a #{values.class.name}"
49 end
50 end
51
30 52 def test_cross_project_activity_filter_should_propose_non_active_activities
31 53 activity = TimeEntryActivity.create!(:name => 'Disabled', :active => false)
32 54 assert !activity.active?
33 55
34 56 query = TimeEntryQuery.new(:name => '_')
35 57 assert options = query.available_filters['activity_id']
36 58 assert values = options[:values]
37 59 assert_include ["Disabled", activity.id.to_s], values
38 60 end
39 61
40 62 def test_activity_filter_should_consider_system_and_project_activities
41 63 TimeEntry.delete_all
42 64 system = TimeEntryActivity.create!(:name => 'Foo')
43 65 TimeEntry.generate!(:activity => system, :hours => 1.0)
44 66 override = TimeEntryActivity.create!(:name => 'Foo', :parent_id => system.id, :project_id => 1)
45 67 other = TimeEntryActivity.create!(:name => 'Bar')
46 68 TimeEntry.generate!(:activity => override, :hours => 2.0)
47 69 TimeEntry.generate!(:activity => other, :hours => 4.0)
48 70
49 71 query = TimeEntryQuery.new(:name => '_')
50 72 query.add_filter('activity_id', '=', [system.id.to_s])
51 73 assert_equal 3.0, query.results_scope.sum(:hours)
52 74
53 75 query = TimeEntryQuery.new(:name => '_')
54 76 query.add_filter('activity_id', '!', [system.id.to_s])
55 77 assert_equal 4.0, query.results_scope.sum(:hours)
56 78 end
57 79
58 80 def test_project_query_should_include_project_issue_custom_fields_only_as_filters
59 81 global = IssueCustomField.generate!(:is_for_all => true, :is_filter => true)
60 82 field_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [3], :is_filter => true)
61 83 field_not_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [1,2], :is_filter => true)
62 84
63 85 query = TimeEntryQuery.new(:project => Project.find(3))
64 86
65 87 assert_include "issue.cf_#{global.id}", query.available_filters.keys
66 88 assert_include "issue.cf_#{field_on_project.id}", query.available_filters.keys
67 89 assert_not_include "issue.cf_#{field_not_on_project.id}", query.available_filters.keys
68 90 end
69 91
70 92 def test_project_query_should_include_project_issue_custom_fields_only_as_columns
71 93 global = IssueCustomField.generate!(:is_for_all => true, :is_filter => true)
72 94 field_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [3], :is_filter => true)
73 95 field_not_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [1,2], :is_filter => true)
74 96
75 97 query = TimeEntryQuery.new(:project => Project.find(3))
76 98
77 99 assert_include "issue.cf_#{global.id}", query.available_columns.map(&:name).map(&:to_s)
78 100 assert_include "issue.cf_#{field_on_project.id}", query.available_columns.map(&:name).map(&:to_s)
79 101 assert_not_include "issue.cf_#{field_not_on_project.id}", query.available_columns.map(&:name).map(&:to_s)
80 102 end
81 103 end
General Comments 0
You need to be logged in to leave comments. Login now