##// END OF EJS Templates
Added a "Member of Group" to the issues filter. #5869...
Eric Davis -
r3963:109b42f4828c
parent child
Show More
@@ -1,596 +1,615
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.default_order = options[:default_order]
30 30 @caption_key = options[:caption] || "field_#{name}"
31 31 end
32 32
33 33 def caption
34 34 l(@caption_key)
35 35 end
36 36
37 37 # Returns true if the column is sortable, otherwise false
38 38 def sortable?
39 39 !sortable.nil?
40 40 end
41 41
42 42 def value(issue)
43 43 issue.send name
44 44 end
45 45 end
46 46
47 47 class QueryCustomFieldColumn < QueryColumn
48 48
49 49 def initialize(custom_field)
50 50 self.name = "cf_#{custom_field.id}".to_sym
51 51 self.sortable = custom_field.order_statement || false
52 52 if %w(list date bool int).include?(custom_field.field_format)
53 53 self.groupable = custom_field.order_statement
54 54 end
55 55 self.groupable ||= false
56 56 @cf = custom_field
57 57 end
58 58
59 59 def caption
60 60 @cf.name
61 61 end
62 62
63 63 def custom_field
64 64 @cf
65 65 end
66 66
67 67 def value(issue)
68 68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
69 69 cv && @cf.cast_value(cv.value)
70 70 end
71 71 end
72 72
73 73 class Query < ActiveRecord::Base
74 74 class StatementInvalid < ::ActiveRecord::StatementInvalid
75 75 end
76 76
77 77 belongs_to :project
78 78 belongs_to :user
79 79 serialize :filters
80 80 serialize :column_names
81 81 serialize :sort_criteria, Array
82 82
83 83 attr_protected :project_id, :user_id
84 84
85 85 validates_presence_of :name, :on => :save
86 86 validates_length_of :name, :maximum => 255
87 87
88 88 @@operators = { "=" => :label_equals,
89 89 "!" => :label_not_equals,
90 90 "o" => :label_open_issues,
91 91 "c" => :label_closed_issues,
92 92 "!*" => :label_none,
93 93 "*" => :label_all,
94 94 ">=" => :label_greater_or_equal,
95 95 "<=" => :label_less_or_equal,
96 96 "<t+" => :label_in_less_than,
97 97 ">t+" => :label_in_more_than,
98 98 "t+" => :label_in,
99 99 "t" => :label_today,
100 100 "w" => :label_this_week,
101 101 ">t-" => :label_less_than_ago,
102 102 "<t-" => :label_more_than_ago,
103 103 "t-" => :label_ago,
104 104 "~" => :label_contains,
105 105 "!~" => :label_not_contains }
106 106
107 107 cattr_reader :operators
108 108
109 109 @@operators_by_filter_type = { :list => [ "=", "!" ],
110 110 :list_status => [ "o", "=", "!", "c", "*" ],
111 111 :list_optional => [ "=", "!", "!*", "*" ],
112 112 :list_subprojects => [ "*", "!*", "=" ],
113 113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
114 114 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
115 115 :string => [ "=", "~", "!", "!~" ],
116 116 :text => [ "~", "!~" ],
117 117 :integer => [ "=", ">=", "<=", "!*", "*" ] }
118 118
119 119 cattr_reader :operators_by_filter_type
120 120
121 121 @@available_columns = [
122 122 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
123 123 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
124 124 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
125 125 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
126 126 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
127 127 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
128 128 QueryColumn.new(:author),
129 129 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
130 130 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
131 131 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
132 132 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
133 133 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
134 134 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
135 135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
136 136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
137 137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
138 138 ]
139 139 cattr_reader :available_columns
140 140
141 141 def initialize(attributes = nil)
142 142 super attributes
143 143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
144 144 end
145 145
146 146 def after_initialize
147 147 # Store the fact that project is nil (used in #editable_by?)
148 148 @is_for_all = project.nil?
149 149 end
150 150
151 151 def validate
152 152 filters.each_key do |field|
153 153 errors.add label_for(field), :blank unless
154 154 # filter requires one or more values
155 155 (values_for(field) and !values_for(field).first.blank?) or
156 156 # filter doesn't require any value
157 157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
158 158 end if filters
159 159 end
160 160
161 161 def editable_by?(user)
162 162 return false unless user
163 163 # Admin can edit them all and regular users can edit their private queries
164 164 return true if user.admin? || (!is_public && self.user_id == user.id)
165 165 # Members can not edit public queries that are for all project (only admin is allowed to)
166 166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
167 167 end
168 168
169 169 def available_filters
170 170 return @available_filters if @available_filters
171 171
172 172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
173 173
174 174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
175 175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
176 176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
177 177 "subject" => { :type => :text, :order => 8 },
178 178 "created_on" => { :type => :date_past, :order => 9 },
179 179 "updated_on" => { :type => :date_past, :order => 10 },
180 180 "start_date" => { :type => :date, :order => 11 },
181 181 "due_date" => { :type => :date, :order => 12 },
182 182 "estimated_hours" => { :type => :integer, :order => 13 },
183 183 "done_ratio" => { :type => :integer, :order => 14 }}
184 184
185 185 user_values = []
186 186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
187 187 if project
188 188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
189 189 else
190 190 project_ids = Project.all(:conditions => Project.visible_by(User.current)).collect(&:id)
191 191 if project_ids.any?
192 192 # members of the user's projects
193 193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] }
194 194 end
195 195 end
196 196 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
197 197 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
198 198
199 group_values = Group.all.collect {|g| [g.name, g.id] }
200 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
201
199 202 if User.current.logged?
200 203 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
201 204 end
202 205
203 206 if project
204 207 # project specific filters
205 208 unless @project.issue_categories.empty?
206 209 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
207 210 end
208 211 unless @project.shared_versions.empty?
209 212 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
210 213 end
211 214 unless @project.descendants.active.empty?
212 215 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
213 216 end
214 217 add_custom_fields_filters(@project.all_issue_custom_fields)
215 218 else
216 219 # global filters for cross project issue list
217 220 system_shared_versions = Version.visible.find_all_by_sharing('system')
218 221 unless system_shared_versions.empty?
219 222 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
220 223 end
221 224 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
222 225 # project filter
223 226 project_values = Project.all(:conditions => Project.visible_by(User.current), :order => 'lft').map do |p|
224 227 pre = (p.level > 0 ? ('--' * p.level + ' ') : '')
225 228 ["#{pre}#{p.name}",p.id.to_s]
226 229 end
227 230 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values}
228 231 end
229 232 @available_filters
230 233 end
231 234
232 235 def add_filter(field, operator, values)
233 236 # values must be an array
234 237 return unless values and values.is_a? Array # and !values.first.empty?
235 238 # check if field is defined as an available filter
236 239 if available_filters.has_key? field
237 240 filter_options = available_filters[field]
238 241 # check if operator is allowed for that filter
239 242 #if @@operators_by_filter_type[filter_options[:type]].include? operator
240 243 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
241 244 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
242 245 #end
243 246 filters[field] = {:operator => operator, :values => values }
244 247 end
245 248 end
246 249
247 250 def add_short_filter(field, expression)
248 251 return unless expression
249 252 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
250 253 add_filter field, (parms[0] || "="), [parms[1] || ""]
251 254 end
252 255
253 256 # Add multiple filters using +add_filter+
254 257 def add_filters(fields, operators, values)
255 258 fields.each do |field|
256 259 add_filter(field, operators[field], values[field])
257 260 end
258 261 end
259 262
260 263 def has_filter?(field)
261 264 filters and filters[field]
262 265 end
263 266
264 267 def operator_for(field)
265 268 has_filter?(field) ? filters[field][:operator] : nil
266 269 end
267 270
268 271 def values_for(field)
269 272 has_filter?(field) ? filters[field][:values] : nil
270 273 end
271 274
272 275 def label_for(field)
273 276 label = available_filters[field][:name] if available_filters.has_key?(field)
274 277 label ||= field.gsub(/\_id$/, "")
275 278 end
276 279
277 280 def available_columns
278 281 return @available_columns if @available_columns
279 282 @available_columns = Query.available_columns
280 283 @available_columns += (project ?
281 284 project.all_issue_custom_fields :
282 285 IssueCustomField.find(:all)
283 286 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
284 287 end
285 288
286 289 def self.available_columns=(v)
287 290 self.available_columns = (v)
288 291 end
289 292
290 293 def self.add_available_column(column)
291 294 self.available_columns << (column) if column.is_a?(QueryColumn)
292 295 end
293 296
294 297 # Returns an array of columns that can be used to group the results
295 298 def groupable_columns
296 299 available_columns.select {|c| c.groupable}
297 300 end
298 301
299 302 # Returns a Hash of columns and the key for sorting
300 303 def sortable_columns
301 304 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
302 305 h[column.name.to_s] = column.sortable
303 306 h
304 307 })
305 308 end
306 309
307 310 def columns
308 311 if has_default_columns?
309 312 available_columns.select do |c|
310 313 # Adds the project column by default for cross-project lists
311 314 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
312 315 end
313 316 else
314 317 # preserve the column_names order
315 318 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
316 319 end
317 320 end
318 321
319 322 def column_names=(names)
320 323 if names
321 324 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
322 325 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
323 326 # Set column_names to nil if default columns
324 327 if names.map(&:to_s) == Setting.issue_list_default_columns
325 328 names = nil
326 329 end
327 330 end
328 331 write_attribute(:column_names, names)
329 332 end
330 333
331 334 def has_column?(column)
332 335 column_names && column_names.include?(column.name)
333 336 end
334 337
335 338 def has_default_columns?
336 339 column_names.nil? || column_names.empty?
337 340 end
338 341
339 342 def sort_criteria=(arg)
340 343 c = []
341 344 if arg.is_a?(Hash)
342 345 arg = arg.keys.sort.collect {|k| arg[k]}
343 346 end
344 347 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
345 348 write_attribute(:sort_criteria, c)
346 349 end
347 350
348 351 def sort_criteria
349 352 read_attribute(:sort_criteria) || []
350 353 end
351 354
352 355 def sort_criteria_key(arg)
353 356 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
354 357 end
355 358
356 359 def sort_criteria_order(arg)
357 360 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
358 361 end
359 362
360 363 # Returns the SQL sort order that should be prepended for grouping
361 364 def group_by_sort_order
362 365 if grouped? && (column = group_by_column)
363 366 column.sortable.is_a?(Array) ?
364 367 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
365 368 "#{column.sortable} #{column.default_order}"
366 369 end
367 370 end
368 371
369 372 # Returns true if the query is a grouped query
370 373 def grouped?
371 374 !group_by.blank?
372 375 end
373 376
374 377 def group_by_column
375 378 groupable_columns.detect {|c| c.name.to_s == group_by}
376 379 end
377 380
378 381 def group_by_statement
379 382 group_by_column.groupable
380 383 end
381 384
382 385 def project_statement
383 386 project_clauses = []
384 387 if project && !@project.descendants.active.empty?
385 388 ids = [project.id]
386 389 if has_filter?("subproject_id")
387 390 case operator_for("subproject_id")
388 391 when '='
389 392 # include the selected subprojects
390 393 ids += values_for("subproject_id").each(&:to_i)
391 394 when '!*'
392 395 # main project only
393 396 else
394 397 # all subprojects
395 398 ids += project.descendants.collect(&:id)
396 399 end
397 400 elsif Setting.display_subprojects_issues?
398 401 ids += project.descendants.collect(&:id)
399 402 end
400 403 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
401 404 elsif project
402 405 project_clauses << "#{Project.table_name}.id = %d" % project.id
403 406 end
404 407 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
405 408 project_clauses.join(' AND ')
406 409 end
407 410
408 411 def statement
409 412 # filters clauses
410 413 filters_clauses = []
411 414 filters.each_key do |field|
412 415 next if field == "subproject_id"
413 416 v = values_for(field).clone
414 417 next unless v and !v.empty?
415 418 operator = operator_for(field)
416 419
417 420 # "me" value subsitution
418 421 if %w(assigned_to_id author_id watcher_id).include?(field)
419 422 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
420 423 end
421 424
422 425 sql = ''
423 426 if field =~ /^cf_(\d+)$/
424 427 # custom field
425 428 db_table = CustomValue.table_name
426 429 db_field = 'value'
427 430 is_custom_filter = true
428 431 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
429 432 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
430 433 elsif field == 'watcher_id'
431 434 db_table = Watcher.table_name
432 435 db_field = 'user_id'
433 436 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
434 437 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
438 elsif field == "member_of_group" # named field
439 if operator == '*' # Any group
440 groups = Group.all
441 members_of_groups = groups.collect(&:user_ids).flatten.compact.collect(&:to_s)
442 operator = '=' # Override the operator since we want to find by assigned_to
443 elsif operator == "!*"
444 groups = Group.all
445 members_of_groups = groups.collect(&:user_ids).flatten.compact.collect(&:to_s)
446 operator = '!' # Override the operator since we want to find by assigned_to
447 else
448 groups = Group.find_all_by_id(v)
449 members_of_groups = groups.collect(&:user_ids).flatten.compact.collect(&:to_s)
450 end
451
452 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
453
435 454 else
436 455 # regular field
437 456 db_table = Issue.table_name
438 457 db_field = field
439 458 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
440 459 end
441 460 filters_clauses << sql
442 461
443 462 end if filters and valid?
444 463
445 464 (filters_clauses << project_statement).join(' AND ')
446 465 end
447 466
448 467 # Returns the issue count
449 468 def issue_count
450 469 Issue.count(:include => [:status, :project], :conditions => statement)
451 470 rescue ::ActiveRecord::StatementInvalid => e
452 471 raise StatementInvalid.new(e.message)
453 472 end
454 473
455 474 # Returns the issue count by group or nil if query is not grouped
456 475 def issue_count_by_group
457 476 r = nil
458 477 if grouped?
459 478 begin
460 479 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
461 480 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
462 481 rescue ActiveRecord::RecordNotFound
463 482 r = {nil => issue_count}
464 483 end
465 484 c = group_by_column
466 485 if c.is_a?(QueryCustomFieldColumn)
467 486 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
468 487 end
469 488 end
470 489 r
471 490 rescue ::ActiveRecord::StatementInvalid => e
472 491 raise StatementInvalid.new(e.message)
473 492 end
474 493
475 494 # Returns the issues
476 495 # Valid options are :order, :offset, :limit, :include, :conditions
477 496 def issues(options={})
478 497 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
479 498 order_option = nil if order_option.blank?
480 499
481 500 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
482 501 :conditions => Query.merge_conditions(statement, options[:conditions]),
483 502 :order => order_option,
484 503 :limit => options[:limit],
485 504 :offset => options[:offset]
486 505 rescue ::ActiveRecord::StatementInvalid => e
487 506 raise StatementInvalid.new(e.message)
488 507 end
489 508
490 509 # Returns the journals
491 510 # Valid options are :order, :offset, :limit
492 511 def journals(options={})
493 512 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
494 513 :conditions => statement,
495 514 :order => options[:order],
496 515 :limit => options[:limit],
497 516 :offset => options[:offset]
498 517 rescue ::ActiveRecord::StatementInvalid => e
499 518 raise StatementInvalid.new(e.message)
500 519 end
501 520
502 521 # Returns the versions
503 522 # Valid options are :conditions
504 523 def versions(options={})
505 524 Version.find :all, :include => :project,
506 525 :conditions => Query.merge_conditions(project_statement, options[:conditions])
507 526 rescue ::ActiveRecord::StatementInvalid => e
508 527 raise StatementInvalid.new(e.message)
509 528 end
510 529
511 530 private
512 531
513 532 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
514 533 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
515 534 sql = ''
516 535 case operator
517 536 when "="
518 537 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
519 538 when "!"
520 539 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
521 540 when "!*"
522 541 sql = "#{db_table}.#{db_field} IS NULL"
523 542 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
524 543 when "*"
525 544 sql = "#{db_table}.#{db_field} IS NOT NULL"
526 545 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
527 546 when ">="
528 547 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
529 548 when "<="
530 549 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
531 550 when "o"
532 551 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
533 552 when "c"
534 553 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
535 554 when ">t-"
536 555 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
537 556 when "<t-"
538 557 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
539 558 when "t-"
540 559 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
541 560 when ">t+"
542 561 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
543 562 when "<t+"
544 563 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
545 564 when "t+"
546 565 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
547 566 when "t"
548 567 sql = date_range_clause(db_table, db_field, 0, 0)
549 568 when "w"
550 569 from = l(:general_first_day_of_week) == '7' ?
551 570 # week starts on sunday
552 571 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
553 572 # week starts on monday (Rails default)
554 573 Time.now.at_beginning_of_week
555 574 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
556 575 when "~"
557 576 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
558 577 when "!~"
559 578 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
560 579 end
561 580
562 581 return sql
563 582 end
564 583
565 584 def add_custom_fields_filters(custom_fields)
566 585 @available_filters ||= {}
567 586
568 587 custom_fields.select(&:is_filter?).each do |field|
569 588 case field.field_format
570 589 when "text"
571 590 options = { :type => :text, :order => 20 }
572 591 when "list"
573 592 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
574 593 when "date"
575 594 options = { :type => :date, :order => 20 }
576 595 when "bool"
577 596 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
578 597 else
579 598 options = { :type => :string, :order => 20 }
580 599 end
581 600 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
582 601 end
583 602 end
584 603
585 604 # Returns a SQL clause for a date or datetime field.
586 605 def date_range_clause(table, field, from, to)
587 606 s = []
588 607 if from
589 608 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
590 609 end
591 610 if to
592 611 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
593 612 end
594 613 s.join(' AND ')
595 614 end
596 615 end
@@ -1,915 +1,916
1 1 en:
2 2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
3 3 direction: ltr
4 4 date:
5 5 formats:
6 6 # Use the strftime parameters for formats.
7 7 # When no format has been given, it uses default.
8 8 # You can provide other formats here if you like!
9 9 default: "%m/%d/%Y"
10 10 short: "%b %d"
11 11 long: "%B %d, %Y"
12 12
13 13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
14 14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
15 15
16 16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
17 17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
18 18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
19 19 # Used in date_select and datime_select.
20 20 order: [ :year, :month, :day ]
21 21
22 22 time:
23 23 formats:
24 24 default: "%m/%d/%Y %I:%M %p"
25 25 time: "%I:%M %p"
26 26 short: "%d %b %H:%M"
27 27 long: "%B %d, %Y %H:%M"
28 28 am: "am"
29 29 pm: "pm"
30 30
31 31 datetime:
32 32 distance_in_words:
33 33 half_a_minute: "half a minute"
34 34 less_than_x_seconds:
35 35 one: "less than 1 second"
36 36 other: "less than {{count}} seconds"
37 37 x_seconds:
38 38 one: "1 second"
39 39 other: "{{count}} seconds"
40 40 less_than_x_minutes:
41 41 one: "less than a minute"
42 42 other: "less than {{count}} minutes"
43 43 x_minutes:
44 44 one: "1 minute"
45 45 other: "{{count}} minutes"
46 46 about_x_hours:
47 47 one: "about 1 hour"
48 48 other: "about {{count}} hours"
49 49 x_days:
50 50 one: "1 day"
51 51 other: "{{count}} days"
52 52 about_x_months:
53 53 one: "about 1 month"
54 54 other: "about {{count}} months"
55 55 x_months:
56 56 one: "1 month"
57 57 other: "{{count}} months"
58 58 about_x_years:
59 59 one: "about 1 year"
60 60 other: "about {{count}} years"
61 61 over_x_years:
62 62 one: "over 1 year"
63 63 other: "over {{count}} years"
64 64 almost_x_years:
65 65 one: "almost 1 year"
66 66 other: "almost {{count}} years"
67 67
68 68 number:
69 69 # Default format for numbers
70 70 format:
71 71 separator: "."
72 72 delimiter: ""
73 73 precision: 3
74 74 human:
75 75 format:
76 76 delimiter: ""
77 77 precision: 1
78 78 storage_units:
79 79 format: "%n %u"
80 80 units:
81 81 byte:
82 82 one: "Byte"
83 83 other: "Bytes"
84 84 kb: "KB"
85 85 mb: "MB"
86 86 gb: "GB"
87 87 tb: "TB"
88 88
89 89
90 90 # Used in array.to_sentence.
91 91 support:
92 92 array:
93 93 sentence_connector: "and"
94 94 skip_last_comma: false
95 95
96 96 activerecord:
97 97 errors:
98 98 messages:
99 99 inclusion: "is not included in the list"
100 100 exclusion: "is reserved"
101 101 invalid: "is invalid"
102 102 confirmation: "doesn't match confirmation"
103 103 accepted: "must be accepted"
104 104 empty: "can't be empty"
105 105 blank: "can't be blank"
106 106 too_long: "is too long (maximum is {{count}} characters)"
107 107 too_short: "is too short (minimum is {{count}} characters)"
108 108 wrong_length: "is the wrong length (should be {{count}} characters)"
109 109 taken: "has already been taken"
110 110 not_a_number: "is not a number"
111 111 not_a_date: "is not a valid date"
112 112 greater_than: "must be greater than {{count}}"
113 113 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
114 114 equal_to: "must be equal to {{count}}"
115 115 less_than: "must be less than {{count}}"
116 116 less_than_or_equal_to: "must be less than or equal to {{count}}"
117 117 odd: "must be odd"
118 118 even: "must be even"
119 119 greater_than_start_date: "must be greater than start date"
120 120 not_same_project: "doesn't belong to the same project"
121 121 circular_dependency: "This relation would create a circular dependency"
122 122 cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks"
123 123
124 124 actionview_instancetag_blank_option: Please select
125 125
126 126 general_text_No: 'No'
127 127 general_text_Yes: 'Yes'
128 128 general_text_no: 'no'
129 129 general_text_yes: 'yes'
130 130 general_lang_name: 'English'
131 131 general_csv_separator: ','
132 132 general_csv_decimal_separator: '.'
133 133 general_csv_encoding: ISO-8859-1
134 134 general_pdf_encoding: ISO-8859-1
135 135 general_first_day_of_week: '7'
136 136
137 137 notice_account_updated: Account was successfully updated.
138 138 notice_account_invalid_creditentials: Invalid user or password
139 139 notice_account_password_updated: Password was successfully updated.
140 140 notice_account_wrong_password: Wrong password
141 141 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
142 142 notice_account_unknown_email: Unknown user.
143 143 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
144 144 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
145 145 notice_account_activated: Your account has been activated. You can now log in.
146 146 notice_successful_create: Successful creation.
147 147 notice_successful_update: Successful update.
148 148 notice_successful_delete: Successful deletion.
149 149 notice_successful_connection: Successful connection.
150 150 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
151 151 notice_locking_conflict: Data has been updated by another user.
152 152 notice_not_authorized: You are not authorized to access this page.
153 153 notice_email_sent: "An email was sent to {{value}}"
154 154 notice_email_error: "An error occurred while sending mail ({{value}})"
155 155 notice_feeds_access_key_reseted: Your RSS access key was reset.
156 156 notice_api_access_key_reseted: Your API access key was reset.
157 157 notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
158 158 notice_failed_to_save_members: "Failed to save member(s): {{errors}}."
159 159 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
160 160 notice_account_pending: "Your account was created and is now pending administrator approval."
161 161 notice_default_data_loaded: Default configuration successfully loaded.
162 162 notice_unable_delete_version: Unable to delete version.
163 163 notice_unable_delete_time_entry: Unable to delete time log entry.
164 164 notice_issue_done_ratios_updated: Issue done ratios updated.
165 165
166 166 error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
167 167 error_scm_not_found: "The entry or revision was not found in the repository."
168 168 error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
169 169 error_scm_annotate: "The entry does not exist or can not be annotated."
170 170 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
171 171 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
172 172 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
173 173 error_can_not_delete_custom_field: Unable to delete custom field
174 174 error_can_not_delete_tracker: "This tracker contains issues and can't be deleted."
175 175 error_can_not_remove_role: "This role is in use and can not be deleted."
176 176 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened'
177 177 error_can_not_archive_project: This project can not be archived
178 178 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
179 179 error_workflow_copy_source: 'Please select a source tracker or role'
180 180 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
181 181 error_unable_delete_issue_status: 'Unable to delete issue status'
182 182 error_unable_to_connect: "Unable to connect ({{value}})"
183 183 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
184 184
185 185 mail_subject_lost_password: "Your {{value}} password"
186 186 mail_body_lost_password: 'To change your password, click on the following link:'
187 187 mail_subject_register: "Your {{value}} account activation"
188 188 mail_body_register: 'To activate your account, click on the following link:'
189 189 mail_body_account_information_external: "You can use your {{value}} account to log in."
190 190 mail_body_account_information: Your account information
191 191 mail_subject_account_activation_request: "{{value}} account activation request"
192 192 mail_body_account_activation_request: "A new user ({{value}}) has registered. The account is pending your approval:"
193 193 mail_subject_reminder: "{{count}} issue(s) due in the next {{days}} days"
194 194 mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
195 195 mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
196 196 mail_body_wiki_content_added: "The '{{page}}' wiki page has been added by {{author}}."
197 197 mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
198 198 mail_body_wiki_content_updated: "The '{{page}}' wiki page has been updated by {{author}}."
199 199
200 200 gui_validation_error: 1 error
201 201 gui_validation_error_plural: "{{count}} errors"
202 202
203 203 field_name: Name
204 204 field_description: Description
205 205 field_summary: Summary
206 206 field_is_required: Required
207 207 field_firstname: Firstname
208 208 field_lastname: Lastname
209 209 field_mail: Email
210 210 field_filename: File
211 211 field_filesize: Size
212 212 field_downloads: Downloads
213 213 field_author: Author
214 214 field_created_on: Created
215 215 field_updated_on: Updated
216 216 field_field_format: Format
217 217 field_is_for_all: For all projects
218 218 field_possible_values: Possible values
219 219 field_regexp: Regular expression
220 220 field_min_length: Minimum length
221 221 field_max_length: Maximum length
222 222 field_value: Value
223 223 field_category: Category
224 224 field_title: Title
225 225 field_project: Project
226 226 field_issue: Issue
227 227 field_status: Status
228 228 field_notes: Notes
229 229 field_is_closed: Issue closed
230 230 field_is_default: Default value
231 231 field_tracker: Tracker
232 232 field_subject: Subject
233 233 field_due_date: Due date
234 234 field_assigned_to: Assignee
235 235 field_priority: Priority
236 236 field_fixed_version: Target version
237 237 field_user: User
238 238 field_principal: Principal
239 239 field_role: Role
240 240 field_homepage: Homepage
241 241 field_is_public: Public
242 242 field_parent: Subproject of
243 243 field_is_in_roadmap: Issues displayed in roadmap
244 244 field_login: Login
245 245 field_mail_notification: Email notifications
246 246 field_admin: Administrator
247 247 field_last_login_on: Last connection
248 248 field_language: Language
249 249 field_effective_date: Date
250 250 field_password: Password
251 251 field_new_password: New password
252 252 field_password_confirmation: Confirmation
253 253 field_version: Version
254 254 field_type: Type
255 255 field_host: Host
256 256 field_port: Port
257 257 field_account: Account
258 258 field_base_dn: Base DN
259 259 field_attr_login: Login attribute
260 260 field_attr_firstname: Firstname attribute
261 261 field_attr_lastname: Lastname attribute
262 262 field_attr_mail: Email attribute
263 263 field_onthefly: On-the-fly user creation
264 264 field_start_date: Start
265 265 field_done_ratio: % Done
266 266 field_auth_source: Authentication mode
267 267 field_hide_mail: Hide my email address
268 268 field_comments: Comment
269 269 field_url: URL
270 270 field_start_page: Start page
271 271 field_subproject: Subproject
272 272 field_hours: Hours
273 273 field_activity: Activity
274 274 field_spent_on: Date
275 275 field_identifier: Identifier
276 276 field_is_filter: Used as a filter
277 277 field_issue_to: Related issue
278 278 field_delay: Delay
279 279 field_assignable: Issues can be assigned to this role
280 280 field_redirect_existing_links: Redirect existing links
281 281 field_estimated_hours: Estimated time
282 282 field_column_names: Columns
283 283 field_time_entries: Log time
284 284 field_time_zone: Time zone
285 285 field_searchable: Searchable
286 286 field_default_value: Default value
287 287 field_comments_sorting: Display comments
288 288 field_parent_title: Parent page
289 289 field_editable: Editable
290 290 field_watcher: Watcher
291 291 field_identity_url: OpenID URL
292 292 field_content: Content
293 293 field_group_by: Group results by
294 294 field_sharing: Sharing
295 295 field_parent_issue: Parent task
296 field_member_of_group: Member of Group
296 297
297 298 setting_app_title: Application title
298 299 setting_app_subtitle: Application subtitle
299 300 setting_welcome_text: Welcome text
300 301 setting_default_language: Default language
301 302 setting_login_required: Authentication required
302 303 setting_self_registration: Self-registration
303 304 setting_attachment_max_size: Attachment max. size
304 305 setting_issues_export_limit: Issues export limit
305 306 setting_mail_from: Emission email address
306 307 setting_bcc_recipients: Blind carbon copy recipients (bcc)
307 308 setting_plain_text_mail: Plain text mail (no HTML)
308 309 setting_host_name: Host name and path
309 310 setting_text_formatting: Text formatting
310 311 setting_wiki_compression: Wiki history compression
311 312 setting_feeds_limit: Feed content limit
312 313 setting_default_projects_public: New projects are public by default
313 314 setting_autofetch_changesets: Autofetch commits
314 315 setting_sys_api_enabled: Enable WS for repository management
315 316 setting_commit_ref_keywords: Referencing keywords
316 317 setting_commit_fix_keywords: Fixing keywords
317 318 setting_autologin: Autologin
318 319 setting_date_format: Date format
319 320 setting_time_format: Time format
320 321 setting_cross_project_issue_relations: Allow cross-project issue relations
321 322 setting_issue_list_default_columns: Default columns displayed on the issue list
322 323 setting_repositories_encodings: Repositories encodings
323 324 setting_commit_logs_encoding: Commit messages encoding
324 325 setting_emails_footer: Emails footer
325 326 setting_protocol: Protocol
326 327 setting_per_page_options: Objects per page options
327 328 setting_user_format: Users display format
328 329 setting_activity_days_default: Days displayed on project activity
329 330 setting_display_subprojects_issues: Display subprojects issues on main projects by default
330 331 setting_enabled_scm: Enabled SCM
331 332 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
332 333 setting_mail_handler_api_enabled: Enable WS for incoming emails
333 334 setting_mail_handler_api_key: API key
334 335 setting_sequential_project_identifiers: Generate sequential project identifiers
335 336 setting_gravatar_enabled: Use Gravatar user icons
336 337 setting_gravatar_default: Default Gravatar image
337 338 setting_diff_max_lines_displayed: Max number of diff lines displayed
338 339 setting_file_max_size_displayed: Max size of text files displayed inline
339 340 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
340 341 setting_openid: Allow OpenID login and registration
341 342 setting_password_min_length: Minimum password length
342 343 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
343 344 setting_default_projects_modules: Default enabled modules for new projects
344 345 setting_issue_done_ratio: Calculate the issue done ratio with
345 346 setting_issue_done_ratio_issue_field: Use the issue field
346 347 setting_issue_done_ratio_issue_status: Use the issue status
347 348 setting_start_of_week: Start calendars on
348 349 setting_rest_api_enabled: Enable REST web service
349 350 setting_cache_formatted_text: Cache formatted text
350 351
351 352 permission_add_project: Create project
352 353 permission_add_subprojects: Create subprojects
353 354 permission_edit_project: Edit project
354 355 permission_select_project_modules: Select project modules
355 356 permission_manage_members: Manage members
356 357 permission_manage_project_activities: Manage project activities
357 358 permission_manage_versions: Manage versions
358 359 permission_manage_categories: Manage issue categories
359 360 permission_view_issues: View Issues
360 361 permission_add_issues: Add issues
361 362 permission_edit_issues: Edit issues
362 363 permission_manage_issue_relations: Manage issue relations
363 364 permission_add_issue_notes: Add notes
364 365 permission_edit_issue_notes: Edit notes
365 366 permission_edit_own_issue_notes: Edit own notes
366 367 permission_move_issues: Move issues
367 368 permission_delete_issues: Delete issues
368 369 permission_manage_public_queries: Manage public queries
369 370 permission_save_queries: Save queries
370 371 permission_view_gantt: View gantt chart
371 372 permission_view_calendar: View calendar
372 373 permission_view_issue_watchers: View watchers list
373 374 permission_add_issue_watchers: Add watchers
374 375 permission_delete_issue_watchers: Delete watchers
375 376 permission_log_time: Log spent time
376 377 permission_view_time_entries: View spent time
377 378 permission_edit_time_entries: Edit time logs
378 379 permission_edit_own_time_entries: Edit own time logs
379 380 permission_manage_news: Manage news
380 381 permission_comment_news: Comment news
381 382 permission_manage_documents: Manage documents
382 383 permission_view_documents: View documents
383 384 permission_manage_files: Manage files
384 385 permission_view_files: View files
385 386 permission_manage_wiki: Manage wiki
386 387 permission_rename_wiki_pages: Rename wiki pages
387 388 permission_delete_wiki_pages: Delete wiki pages
388 389 permission_view_wiki_pages: View wiki
389 390 permission_view_wiki_edits: View wiki history
390 391 permission_edit_wiki_pages: Edit wiki pages
391 392 permission_delete_wiki_pages_attachments: Delete attachments
392 393 permission_protect_wiki_pages: Protect wiki pages
393 394 permission_manage_repository: Manage repository
394 395 permission_browse_repository: Browse repository
395 396 permission_view_changesets: View changesets
396 397 permission_commit_access: Commit access
397 398 permission_manage_boards: Manage boards
398 399 permission_view_messages: View messages
399 400 permission_add_messages: Post messages
400 401 permission_edit_messages: Edit messages
401 402 permission_edit_own_messages: Edit own messages
402 403 permission_delete_messages: Delete messages
403 404 permission_delete_own_messages: Delete own messages
404 405 permission_export_wiki_pages: Export wiki pages
405 406 permission_manage_subtasks: Manage subtasks
406 407
407 408 project_module_issue_tracking: Issue tracking
408 409 project_module_time_tracking: Time tracking
409 410 project_module_news: News
410 411 project_module_documents: Documents
411 412 project_module_files: Files
412 413 project_module_wiki: Wiki
413 414 project_module_repository: Repository
414 415 project_module_boards: Boards
415 416 project_module_calendar: Calendar
416 417 project_module_gantt: Gantt
417 418
418 419 label_user: User
419 420 label_user_plural: Users
420 421 label_user_new: New user
421 422 label_user_anonymous: Anonymous
422 423 label_project: Project
423 424 label_project_new: New project
424 425 label_project_plural: Projects
425 426 label_x_projects:
426 427 zero: no projects
427 428 one: 1 project
428 429 other: "{{count}} projects"
429 430 label_project_all: All Projects
430 431 label_project_latest: Latest projects
431 432 label_issue: Issue
432 433 label_issue_new: New issue
433 434 label_issue_plural: Issues
434 435 label_issue_view_all: View all issues
435 436 label_issues_by: "Issues by {{value}}"
436 437 label_issue_added: Issue added
437 438 label_issue_updated: Issue updated
438 439 label_document: Document
439 440 label_document_new: New document
440 441 label_document_plural: Documents
441 442 label_document_added: Document added
442 443 label_role: Role
443 444 label_role_plural: Roles
444 445 label_role_new: New role
445 446 label_role_and_permissions: Roles and permissions
446 447 label_member: Member
447 448 label_member_new: New member
448 449 label_member_plural: Members
449 450 label_tracker: Tracker
450 451 label_tracker_plural: Trackers
451 452 label_tracker_new: New tracker
452 453 label_workflow: Workflow
453 454 label_issue_status: Issue status
454 455 label_issue_status_plural: Issue statuses
455 456 label_issue_status_new: New status
456 457 label_issue_category: Issue category
457 458 label_issue_category_plural: Issue categories
458 459 label_issue_category_new: New category
459 460 label_custom_field: Custom field
460 461 label_custom_field_plural: Custom fields
461 462 label_custom_field_new: New custom field
462 463 label_enumerations: Enumerations
463 464 label_enumeration_new: New value
464 465 label_information: Information
465 466 label_information_plural: Information
466 467 label_please_login: Please log in
467 468 label_register: Register
468 469 label_login_with_open_id_option: or login with OpenID
469 470 label_password_lost: Lost password
470 471 label_home: Home
471 472 label_my_page: My page
472 473 label_my_account: My account
473 474 label_my_projects: My projects
474 475 label_my_page_block: My page block
475 476 label_administration: Administration
476 477 label_login: Sign in
477 478 label_logout: Sign out
478 479 label_help: Help
479 480 label_reported_issues: Reported issues
480 481 label_assigned_to_me_issues: Issues assigned to me
481 482 label_last_login: Last connection
482 483 label_registered_on: Registered on
483 484 label_activity: Activity
484 485 label_overall_activity: Overall activity
485 486 label_user_activity: "{{value}}'s activity"
486 487 label_new: New
487 488 label_logged_as: Logged in as
488 489 label_environment: Environment
489 490 label_authentication: Authentication
490 491 label_auth_source: Authentication mode
491 492 label_auth_source_new: New authentication mode
492 493 label_auth_source_plural: Authentication modes
493 494 label_subproject_plural: Subprojects
494 495 label_subproject_new: New subproject
495 496 label_and_its_subprojects: "{{value}} and its subprojects"
496 497 label_min_max_length: Min - Max length
497 498 label_list: List
498 499 label_date: Date
499 500 label_integer: Integer
500 501 label_float: Float
501 502 label_boolean: Boolean
502 503 label_string: Text
503 504 label_text: Long text
504 505 label_attribute: Attribute
505 506 label_attribute_plural: Attributes
506 507 label_download: "{{count}} Download"
507 508 label_download_plural: "{{count}} Downloads"
508 509 label_no_data: No data to display
509 510 label_change_status: Change status
510 511 label_history: History
511 512 label_attachment: File
512 513 label_attachment_new: New file
513 514 label_attachment_delete: Delete file
514 515 label_attachment_plural: Files
515 516 label_file_added: File added
516 517 label_report: Report
517 518 label_report_plural: Reports
518 519 label_news: News
519 520 label_news_new: Add news
520 521 label_news_plural: News
521 522 label_news_latest: Latest news
522 523 label_news_view_all: View all news
523 524 label_news_added: News added
524 525 label_settings: Settings
525 526 label_overview: Overview
526 527 label_version: Version
527 528 label_version_new: New version
528 529 label_version_plural: Versions
529 530 label_close_versions: Close completed versions
530 531 label_confirmation: Confirmation
531 532 label_export_to: 'Also available in:'
532 533 label_read: Read...
533 534 label_public_projects: Public projects
534 535 label_open_issues: open
535 536 label_open_issues_plural: open
536 537 label_closed_issues: closed
537 538 label_closed_issues_plural: closed
538 539 label_x_open_issues_abbr_on_total:
539 540 zero: 0 open / {{total}}
540 541 one: 1 open / {{total}}
541 542 other: "{{count}} open / {{total}}"
542 543 label_x_open_issues_abbr:
543 544 zero: 0 open
544 545 one: 1 open
545 546 other: "{{count}} open"
546 547 label_x_closed_issues_abbr:
547 548 zero: 0 closed
548 549 one: 1 closed
549 550 other: "{{count}} closed"
550 551 label_total: Total
551 552 label_permissions: Permissions
552 553 label_current_status: Current status
553 554 label_new_statuses_allowed: New statuses allowed
554 555 label_all: all
555 556 label_none: none
556 557 label_nobody: nobody
557 558 label_next: Next
558 559 label_previous: Previous
559 560 label_used_by: Used by
560 561 label_details: Details
561 562 label_add_note: Add a note
562 563 label_per_page: Per page
563 564 label_calendar: Calendar
564 565 label_months_from: months from
565 566 label_gantt: Gantt
566 567 label_internal: Internal
567 568 label_last_changes: "last {{count}} changes"
568 569 label_change_view_all: View all changes
569 570 label_personalize_page: Personalize this page
570 571 label_comment: Comment
571 572 label_comment_plural: Comments
572 573 label_x_comments:
573 574 zero: no comments
574 575 one: 1 comment
575 576 other: "{{count}} comments"
576 577 label_comment_add: Add a comment
577 578 label_comment_added: Comment added
578 579 label_comment_delete: Delete comments
579 580 label_query: Custom query
580 581 label_query_plural: Custom queries
581 582 label_query_new: New query
582 583 label_filter_add: Add filter
583 584 label_filter_plural: Filters
584 585 label_equals: is
585 586 label_not_equals: is not
586 587 label_in_less_than: in less than
587 588 label_in_more_than: in more than
588 589 label_greater_or_equal: '>='
589 590 label_less_or_equal: '<='
590 591 label_in: in
591 592 label_today: today
592 593 label_all_time: all time
593 594 label_yesterday: yesterday
594 595 label_this_week: this week
595 596 label_last_week: last week
596 597 label_last_n_days: "last {{count}} days"
597 598 label_this_month: this month
598 599 label_last_month: last month
599 600 label_this_year: this year
600 601 label_date_range: Date range
601 602 label_less_than_ago: less than days ago
602 603 label_more_than_ago: more than days ago
603 604 label_ago: days ago
604 605 label_contains: contains
605 606 label_not_contains: doesn't contain
606 607 label_day_plural: days
607 608 label_repository: Repository
608 609 label_repository_plural: Repositories
609 610 label_browse: Browse
610 611 label_modification: "{{count}} change"
611 612 label_modification_plural: "{{count}} changes"
612 613 label_branch: Branch
613 614 label_tag: Tag
614 615 label_revision: Revision
615 616 label_revision_plural: Revisions
616 617 label_revision_id: "Revision {{value}}"
617 618 label_associated_revisions: Associated revisions
618 619 label_added: added
619 620 label_modified: modified
620 621 label_copied: copied
621 622 label_renamed: renamed
622 623 label_deleted: deleted
623 624 label_latest_revision: Latest revision
624 625 label_latest_revision_plural: Latest revisions
625 626 label_view_revisions: View revisions
626 627 label_view_all_revisions: View all revisions
627 628 label_max_size: Maximum size
628 629 label_sort_highest: Move to top
629 630 label_sort_higher: Move up
630 631 label_sort_lower: Move down
631 632 label_sort_lowest: Move to bottom
632 633 label_roadmap: Roadmap
633 634 label_roadmap_due_in: "Due in {{value}}"
634 635 label_roadmap_overdue: "{{value}} late"
635 636 label_roadmap_no_issues: No issues for this version
636 637 label_search: Search
637 638 label_result_plural: Results
638 639 label_all_words: All words
639 640 label_wiki: Wiki
640 641 label_wiki_edit: Wiki edit
641 642 label_wiki_edit_plural: Wiki edits
642 643 label_wiki_page: Wiki page
643 644 label_wiki_page_plural: Wiki pages
644 645 label_index_by_title: Index by title
645 646 label_index_by_date: Index by date
646 647 label_current_version: Current version
647 648 label_preview: Preview
648 649 label_feed_plural: Feeds
649 650 label_changes_details: Details of all changes
650 651 label_issue_tracking: Issue tracking
651 652 label_spent_time: Spent time
652 653 label_overall_spent_time: Overall spent time
653 654 label_f_hour: "{{value}} hour"
654 655 label_f_hour_plural: "{{value}} hours"
655 656 label_time_tracking: Time tracking
656 657 label_change_plural: Changes
657 658 label_statistics: Statistics
658 659 label_commits_per_month: Commits per month
659 660 label_commits_per_author: Commits per author
660 661 label_view_diff: View differences
661 662 label_diff_inline: inline
662 663 label_diff_side_by_side: side by side
663 664 label_options: Options
664 665 label_copy_workflow_from: Copy workflow from
665 666 label_permissions_report: Permissions report
666 667 label_watched_issues: Watched issues
667 668 label_related_issues: Related issues
668 669 label_applied_status: Applied status
669 670 label_loading: Loading...
670 671 label_relation_new: New relation
671 672 label_relation_delete: Delete relation
672 673 label_relates_to: related to
673 674 label_duplicates: duplicates
674 675 label_duplicated_by: duplicated by
675 676 label_blocks: blocks
676 677 label_blocked_by: blocked by
677 678 label_precedes: precedes
678 679 label_follows: follows
679 680 label_end_to_start: end to start
680 681 label_end_to_end: end to end
681 682 label_start_to_start: start to start
682 683 label_start_to_end: start to end
683 684 label_stay_logged_in: Stay logged in
684 685 label_disabled: disabled
685 686 label_show_completed_versions: Show completed versions
686 687 label_me: me
687 688 label_board: Forum
688 689 label_board_new: New forum
689 690 label_board_plural: Forums
690 691 label_board_locked: Locked
691 692 label_board_sticky: Sticky
692 693 label_topic_plural: Topics
693 694 label_message_plural: Messages
694 695 label_message_last: Last message
695 696 label_message_new: New message
696 697 label_message_posted: Message added
697 698 label_reply_plural: Replies
698 699 label_send_information: Send account information to the user
699 700 label_year: Year
700 701 label_month: Month
701 702 label_week: Week
702 703 label_date_from: From
703 704 label_date_to: To
704 705 label_language_based: Based on user's language
705 706 label_sort_by: "Sort by {{value}}"
706 707 label_send_test_email: Send a test email
707 708 label_feeds_access_key: RSS access key
708 709 label_missing_feeds_access_key: Missing a RSS access key
709 710 label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
710 711 label_module_plural: Modules
711 712 label_added_time_by: "Added by {{author}} {{age}} ago"
712 713 label_updated_time_by: "Updated by {{author}} {{age}} ago"
713 714 label_updated_time: "Updated {{value}} ago"
714 715 label_jump_to_a_project: Jump to a project...
715 716 label_file_plural: Files
716 717 label_changeset_plural: Changesets
717 718 label_default_columns: Default columns
718 719 label_no_change_option: (No change)
719 720 label_bulk_edit_selected_issues: Bulk edit selected issues
720 721 label_theme: Theme
721 722 label_default: Default
722 723 label_search_titles_only: Search titles only
723 724 label_user_mail_option_all: "For any event on all my projects"
724 725 label_user_mail_option_selected: "For any event on the selected projects only..."
725 726 label_user_mail_option_none: "Only for things I watch or I'm involved in"
726 727 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
727 728 label_registration_activation_by_email: account activation by email
728 729 label_registration_manual_activation: manual account activation
729 730 label_registration_automatic_activation: automatic account activation
730 731 label_display_per_page: "Per page: {{value}}"
731 732 label_age: Age
732 733 label_change_properties: Change properties
733 734 label_general: General
734 735 label_more: More
735 736 label_scm: SCM
736 737 label_plugins: Plugins
737 738 label_ldap_authentication: LDAP authentication
738 739 label_downloads_abbr: D/L
739 740 label_optional_description: Optional description
740 741 label_add_another_file: Add another file
741 742 label_preferences: Preferences
742 743 label_chronological_order: In chronological order
743 744 label_reverse_chronological_order: In reverse chronological order
744 745 label_planning: Planning
745 746 label_incoming_emails: Incoming emails
746 747 label_generate_key: Generate a key
747 748 label_issue_watchers: Watchers
748 749 label_example: Example
749 750 label_display: Display
750 751 label_sort: Sort
751 752 label_ascending: Ascending
752 753 label_descending: Descending
753 754 label_date_from_to: From {{start}} to {{end}}
754 755 label_wiki_content_added: Wiki page added
755 756 label_wiki_content_updated: Wiki page updated
756 757 label_group: Group
757 758 label_group_plural: Groups
758 759 label_group_new: New group
759 760 label_time_entry_plural: Spent time
760 761 label_version_sharing_none: Not shared
761 762 label_version_sharing_descendants: With subprojects
762 763 label_version_sharing_hierarchy: With project hierarchy
763 764 label_version_sharing_tree: With project tree
764 765 label_version_sharing_system: With all projects
765 766 label_update_issue_done_ratios: Update issue done ratios
766 767 label_copy_source: Source
767 768 label_copy_target: Target
768 769 label_copy_same_as_target: Same as target
769 770 label_display_used_statuses_only: Only display statuses that are used by this tracker
770 771 label_api_access_key: API access key
771 772 label_missing_api_access_key: Missing an API access key
772 773 label_api_access_key_created_on: "API access key created {{value}} ago"
773 774 label_profile: Profile
774 775 label_subtask_plural: Subtasks
775 776 label_project_copy_notifications: Send email notifications during the project copy
776 777
777 778 button_login: Login
778 779 button_submit: Submit
779 780 button_save: Save
780 781 button_check_all: Check all
781 782 button_uncheck_all: Uncheck all
782 783 button_delete: Delete
783 784 button_create: Create
784 785 button_create_and_continue: Create and continue
785 786 button_test: Test
786 787 button_edit: Edit
787 788 button_add: Add
788 789 button_change: Change
789 790 button_apply: Apply
790 791 button_clear: Clear
791 792 button_lock: Lock
792 793 button_unlock: Unlock
793 794 button_download: Download
794 795 button_list: List
795 796 button_view: View
796 797 button_move: Move
797 798 button_move_and_follow: Move and follow
798 799 button_back: Back
799 800 button_cancel: Cancel
800 801 button_activate: Activate
801 802 button_sort: Sort
802 803 button_log_time: Log time
803 804 button_rollback: Rollback to this version
804 805 button_watch: Watch
805 806 button_unwatch: Unwatch
806 807 button_reply: Reply
807 808 button_archive: Archive
808 809 button_unarchive: Unarchive
809 810 button_reset: Reset
810 811 button_rename: Rename
811 812 button_change_password: Change password
812 813 button_copy: Copy
813 814 button_copy_and_follow: Copy and follow
814 815 button_annotate: Annotate
815 816 button_update: Update
816 817 button_configure: Configure
817 818 button_quote: Quote
818 819 button_duplicate: Duplicate
819 820 button_show: Show
820 821
821 822 status_active: active
822 823 status_registered: registered
823 824 status_locked: locked
824 825
825 826 version_status_open: open
826 827 version_status_locked: locked
827 828 version_status_closed: closed
828 829
829 830 field_active: Active
830 831
831 832 text_select_mail_notifications: Select actions for which email notifications should be sent.
832 833 text_regexp_info: eg. ^[A-Z0-9]+$
833 834 text_min_max_length_info: 0 means no restriction
834 835 text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
835 836 text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
836 837 text_workflow_edit: Select a role and a tracker to edit the workflow
837 838 text_are_you_sure: Are you sure ?
838 839 text_journal_changed: "{{label}} changed from {{old}} to {{new}}"
839 840 text_journal_set_to: "{{label}} set to {{value}}"
840 841 text_journal_deleted: "{{label}} deleted ({{old}})"
841 842 text_journal_added: "{{label}} {{value}} added"
842 843 text_tip_task_begin_day: task beginning this day
843 844 text_tip_task_end_day: task ending this day
844 845 text_tip_task_begin_end_day: task beginning and ending this day
845 846 text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.'
846 847 text_caracters_maximum: "{{count}} characters maximum."
847 848 text_caracters_minimum: "Must be at least {{count}} characters long."
848 849 text_length_between: "Length between {{min}} and {{max}} characters."
849 850 text_tracker_no_workflow: No workflow defined for this tracker
850 851 text_unallowed_characters: Unallowed characters
851 852 text_comma_separated: Multiple values allowed (comma separated).
852 853 text_line_separated: Multiple values allowed (one line for each value).
853 854 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
854 855 text_issue_added: "Issue {{id}} has been reported by {{author}}."
855 856 text_issue_updated: "Issue {{id}} has been updated by {{author}}."
856 857 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
857 858 text_issue_category_destroy_question: "Some issues ({{count}}) are assigned to this category. What do you want to do ?"
858 859 text_issue_category_destroy_assignments: Remove category assignments
859 860 text_issue_category_reassign_to: Reassign issues to this category
860 861 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
861 862 text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
862 863 text_load_default_configuration: Load the default configuration
863 864 text_status_changed_by_changeset: "Applied in changeset {{value}}."
864 865 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
865 866 text_select_project_modules: 'Select modules to enable for this project:'
866 867 text_default_administrator_account_changed: Default administrator account changed
867 868 text_file_repository_writable: Attachments directory writable
868 869 text_plugin_assets_writable: Plugin assets directory writable
869 870 text_rmagick_available: RMagick available (optional)
870 871 text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
871 872 text_destroy_time_entries: Delete reported hours
872 873 text_assign_time_entries_to_project: Assign reported hours to the project
873 874 text_reassign_time_entries: 'Reassign reported hours to this issue:'
874 875 text_user_wrote: "{{value}} wrote:"
875 876 text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
876 877 text_enumeration_category_reassign_to: 'Reassign them to this value:'
877 878 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
878 879 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
879 880 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
880 881 text_custom_field_possible_values_info: 'One line for each value'
881 882 text_wiki_page_destroy_question: "This page has {{descendants}} child page(s) and descendant(s). What do you want to do?"
882 883 text_wiki_page_nullify_children: "Keep child pages as root pages"
883 884 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
884 885 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
885 886 text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
886 887 text_zoom_in: Zoom in
887 888 text_zoom_out: Zoom out
888 889
889 890 default_role_manager: Manager
890 891 default_role_developer: Developer
891 892 default_role_reporter: Reporter
892 893 default_tracker_bug: Bug
893 894 default_tracker_feature: Feature
894 895 default_tracker_support: Support
895 896 default_issue_status_new: New
896 897 default_issue_status_in_progress: In Progress
897 898 default_issue_status_resolved: Resolved
898 899 default_issue_status_feedback: Feedback
899 900 default_issue_status_closed: Closed
900 901 default_issue_status_rejected: Rejected
901 902 default_doc_category_user: User documentation
902 903 default_doc_category_tech: Technical documentation
903 904 default_priority_low: Low
904 905 default_priority_normal: Normal
905 906 default_priority_high: High
906 907 default_priority_urgent: Urgent
907 908 default_priority_immediate: Immediate
908 909 default_activity_design: Design
909 910 default_activity_development: Development
910 911
911 912 enumeration_issue_priorities: Issue priorities
912 913 enumeration_doc_categories: Document categories
913 914 enumeration_activities: Activities (time tracking)
914 915 enumeration_system_activity: System Activity
915 916
@@ -1,372 +1,458
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class QueryTest < ActiveSupport::TestCase
21 21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
22 22
23 23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 24 query = Query.new(:project => nil, :name => '_')
25 25 assert query.available_filters.has_key?('cf_1')
26 26 assert !query.available_filters.has_key?('cf_3')
27 27 end
28 28
29 29 def test_system_shared_versions_should_be_available_in_global_queries
30 30 Version.find(2).update_attribute :sharing, 'system'
31 31 query = Query.new(:project => nil, :name => '_')
32 32 assert query.available_filters.has_key?('fixed_version_id')
33 33 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
34 34 end
35 35
36 36 def test_project_filter_in_global_queries
37 37 query = Query.new(:project => nil, :name => '_')
38 38 project_filter = query.available_filters["project_id"]
39 39 assert_not_nil project_filter
40 40 project_ids = project_filter[:values].map{|p| p[1]}
41 41 assert project_ids.include?("1") #public project
42 42 assert !project_ids.include?("2") #private project user cannot see
43 43 end
44 44
45 45 def find_issues_with_query(query)
46 46 Issue.find :all,
47 47 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
48 48 :conditions => query.statement
49 49 end
50 50
51 def assert_find_issues_with_query_is_successful(query)
52 assert_nothing_raised do
53 find_issues_with_query(query)
54 end
55 end
56
57 def assert_query_statement_includes(query, condition)
58 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
59 end
60
51 61 def test_query_should_allow_shared_versions_for_a_project_query
52 62 subproject_version = Version.find(4)
53 63 query = Query.new(:project => Project.find(1), :name => '_')
54 64 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
55 65
56 66 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
57 67 end
58 68
59 69 def test_query_with_multiple_custom_fields
60 70 query = Query.find(1)
61 71 assert query.valid?
62 72 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
63 73 issues = find_issues_with_query(query)
64 74 assert_equal 1, issues.length
65 75 assert_equal Issue.find(3), issues.first
66 76 end
67 77
68 78 def test_operator_none
69 79 query = Query.new(:project => Project.find(1), :name => '_')
70 80 query.add_filter('fixed_version_id', '!*', [''])
71 81 query.add_filter('cf_1', '!*', [''])
72 82 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
73 83 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
74 84 find_issues_with_query(query)
75 85 end
76 86
77 87 def test_operator_none_for_integer
78 88 query = Query.new(:project => Project.find(1), :name => '_')
79 89 query.add_filter('estimated_hours', '!*', [''])
80 90 issues = find_issues_with_query(query)
81 91 assert !issues.empty?
82 92 assert issues.all? {|i| !i.estimated_hours}
83 93 end
84 94
85 95 def test_operator_all
86 96 query = Query.new(:project => Project.find(1), :name => '_')
87 97 query.add_filter('fixed_version_id', '*', [''])
88 98 query.add_filter('cf_1', '*', [''])
89 99 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
90 100 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
91 101 find_issues_with_query(query)
92 102 end
93 103
94 104 def test_operator_greater_than
95 105 query = Query.new(:project => Project.find(1), :name => '_')
96 106 query.add_filter('done_ratio', '>=', ['40'])
97 107 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
98 108 find_issues_with_query(query)
99 109 end
100 110
101 111 def test_operator_in_more_than
102 112 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
103 113 query = Query.new(:project => Project.find(1), :name => '_')
104 114 query.add_filter('due_date', '>t+', ['15'])
105 115 issues = find_issues_with_query(query)
106 116 assert !issues.empty?
107 117 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
108 118 end
109 119
110 120 def test_operator_in_less_than
111 121 query = Query.new(:project => Project.find(1), :name => '_')
112 122 query.add_filter('due_date', '<t+', ['15'])
113 123 issues = find_issues_with_query(query)
114 124 assert !issues.empty?
115 125 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
116 126 end
117 127
118 128 def test_operator_less_than_ago
119 129 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
120 130 query = Query.new(:project => Project.find(1), :name => '_')
121 131 query.add_filter('due_date', '>t-', ['3'])
122 132 issues = find_issues_with_query(query)
123 133 assert !issues.empty?
124 134 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
125 135 end
126 136
127 137 def test_operator_more_than_ago
128 138 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
129 139 query = Query.new(:project => Project.find(1), :name => '_')
130 140 query.add_filter('due_date', '<t-', ['10'])
131 141 assert query.statement.include?("#{Issue.table_name}.due_date <=")
132 142 issues = find_issues_with_query(query)
133 143 assert !issues.empty?
134 144 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
135 145 end
136 146
137 147 def test_operator_in
138 148 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
139 149 query = Query.new(:project => Project.find(1), :name => '_')
140 150 query.add_filter('due_date', 't+', ['2'])
141 151 issues = find_issues_with_query(query)
142 152 assert !issues.empty?
143 153 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
144 154 end
145 155
146 156 def test_operator_ago
147 157 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
148 158 query = Query.new(:project => Project.find(1), :name => '_')
149 159 query.add_filter('due_date', 't-', ['3'])
150 160 issues = find_issues_with_query(query)
151 161 assert !issues.empty?
152 162 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
153 163 end
154 164
155 165 def test_operator_today
156 166 query = Query.new(:project => Project.find(1), :name => '_')
157 167 query.add_filter('due_date', 't', [''])
158 168 issues = find_issues_with_query(query)
159 169 assert !issues.empty?
160 170 issues.each {|issue| assert_equal Date.today, issue.due_date}
161 171 end
162 172
163 173 def test_operator_this_week_on_date
164 174 query = Query.new(:project => Project.find(1), :name => '_')
165 175 query.add_filter('due_date', 'w', [''])
166 176 find_issues_with_query(query)
167 177 end
168 178
169 179 def test_operator_this_week_on_datetime
170 180 query = Query.new(:project => Project.find(1), :name => '_')
171 181 query.add_filter('created_on', 'w', [''])
172 182 find_issues_with_query(query)
173 183 end
174 184
175 185 def test_operator_contains
176 186 query = Query.new(:project => Project.find(1), :name => '_')
177 187 query.add_filter('subject', '~', ['uNable'])
178 188 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
179 189 result = find_issues_with_query(query)
180 190 assert result.empty?
181 191 result.each {|issue| assert issue.subject.downcase.include?('unable') }
182 192 end
183 193
184 194 def test_operator_does_not_contains
185 195 query = Query.new(:project => Project.find(1), :name => '_')
186 196 query.add_filter('subject', '!~', ['uNable'])
187 197 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
188 198 find_issues_with_query(query)
189 199 end
190 200
191 201 def test_filter_watched_issues
192 202 User.current = User.find(1)
193 203 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
194 204 result = find_issues_with_query(query)
195 205 assert_not_nil result
196 206 assert !result.empty?
197 207 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
198 208 User.current = nil
199 209 end
200 210
201 211 def test_filter_unwatched_issues
202 212 User.current = User.find(1)
203 213 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
204 214 result = find_issues_with_query(query)
205 215 assert_not_nil result
206 216 assert !result.empty?
207 217 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
208 218 User.current = nil
209 219 end
210 220
211 221 def test_default_columns
212 222 q = Query.new
213 223 assert !q.columns.empty?
214 224 end
215 225
216 226 def test_set_column_names
217 227 q = Query.new
218 228 q.column_names = ['tracker', :subject, '', 'unknonw_column']
219 229 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
220 230 c = q.columns.first
221 231 assert q.has_column?(c)
222 232 end
223 233
224 234 def test_groupable_columns_should_include_custom_fields
225 235 q = Query.new
226 236 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
227 237 end
228 238
229 239 def test_default_sort
230 240 q = Query.new
231 241 assert_equal [], q.sort_criteria
232 242 end
233 243
234 244 def test_set_sort_criteria_with_hash
235 245 q = Query.new
236 246 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
237 247 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
238 248 end
239 249
240 250 def test_set_sort_criteria_with_array
241 251 q = Query.new
242 252 q.sort_criteria = [['priority', 'desc'], 'tracker']
243 253 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
244 254 end
245 255
246 256 def test_create_query_with_sort
247 257 q = Query.new(:name => 'Sorted')
248 258 q.sort_criteria = [['priority', 'desc'], 'tracker']
249 259 assert q.save
250 260 q.reload
251 261 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
252 262 end
253 263
254 264 def test_sort_by_string_custom_field_asc
255 265 q = Query.new
256 266 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
257 267 assert c
258 268 assert c.sortable
259 269 issues = Issue.find :all,
260 270 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
261 271 :conditions => q.statement,
262 272 :order => "#{c.sortable} ASC"
263 273 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
264 274 assert !values.empty?
265 275 assert_equal values.sort, values
266 276 end
267 277
268 278 def test_sort_by_string_custom_field_desc
269 279 q = Query.new
270 280 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
271 281 assert c
272 282 assert c.sortable
273 283 issues = Issue.find :all,
274 284 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
275 285 :conditions => q.statement,
276 286 :order => "#{c.sortable} DESC"
277 287 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
278 288 assert !values.empty?
279 289 assert_equal values.sort.reverse, values
280 290 end
281 291
282 292 def test_sort_by_float_custom_field_asc
283 293 q = Query.new
284 294 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
285 295 assert c
286 296 assert c.sortable
287 297 issues = Issue.find :all,
288 298 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
289 299 :conditions => q.statement,
290 300 :order => "#{c.sortable} ASC"
291 301 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
292 302 assert !values.empty?
293 303 assert_equal values.sort, values
294 304 end
295 305
296 306 def test_invalid_query_should_raise_query_statement_invalid_error
297 307 q = Query.new
298 308 assert_raise Query::StatementInvalid do
299 309 q.issues(:conditions => "foo = 1")
300 310 end
301 311 end
302 312
303 313 def test_issue_count_by_association_group
304 314 q = Query.new(:name => '_', :group_by => 'assigned_to')
305 315 count_by_group = q.issue_count_by_group
306 316 assert_kind_of Hash, count_by_group
307 317 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
308 318 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
309 319 assert count_by_group.has_key?(User.find(3))
310 320 end
311 321
312 322 def test_issue_count_by_list_custom_field_group
313 323 q = Query.new(:name => '_', :group_by => 'cf_1')
314 324 count_by_group = q.issue_count_by_group
315 325 assert_kind_of Hash, count_by_group
316 326 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
317 327 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
318 328 assert count_by_group.has_key?('MySQL')
319 329 end
320 330
321 331 def test_issue_count_by_date_custom_field_group
322 332 q = Query.new(:name => '_', :group_by => 'cf_8')
323 333 count_by_group = q.issue_count_by_group
324 334 assert_kind_of Hash, count_by_group
325 335 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
326 336 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
327 337 end
328 338
329 339 def test_label_for
330 340 q = Query.new
331 341 assert_equal 'assigned_to', q.label_for('assigned_to_id')
332 342 end
333 343
334 344 def test_editable_by
335 345 admin = User.find(1)
336 346 manager = User.find(2)
337 347 developer = User.find(3)
338 348
339 349 # Public query on project 1
340 350 q = Query.find(1)
341 351 assert q.editable_by?(admin)
342 352 assert q.editable_by?(manager)
343 353 assert !q.editable_by?(developer)
344 354
345 355 # Private query on project 1
346 356 q = Query.find(2)
347 357 assert q.editable_by?(admin)
348 358 assert !q.editable_by?(manager)
349 359 assert q.editable_by?(developer)
350 360
351 361 # Private query for all projects
352 362 q = Query.find(3)
353 363 assert q.editable_by?(admin)
354 364 assert !q.editable_by?(manager)
355 365 assert q.editable_by?(developer)
356 366
357 367 # Public query for all projects
358 368 q = Query.find(4)
359 369 assert q.editable_by?(admin)
360 370 assert !q.editable_by?(manager)
361 371 assert !q.editable_by?(developer)
362 372 end
363 373
364 374 context "#available_filters" do
375 setup do
376 @query = Query.new(:name => "_")
377 end
378
365 379 should "include users of visible projects in cross-project view" do
366 query = Query.new(:name => "_")
367 users = query.available_filters["assigned_to_id"]
380 users = @query.available_filters["assigned_to_id"]
368 381 assert_not_nil users
369 382 assert users[:values].map{|u|u[1]}.include?("3")
370 383 end
384
385 context "'member_of_group' filter" do
386 should "be present" do
387 assert @query.available_filters.keys.include?("member_of_group")
371 388 end
389
390 should "be an optional list" do
391 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
392 end
393
394 should "have a list of the groups as values" do
395 Group.destroy_all # No fixtures
396 group1 = Group.generate!.reload
397 group2 = Group.generate!.reload
398
399 expected_group_list = [
400 [group1.name, group1.id],
401 [group2.name, group2.id]
402 ]
403 assert_equal expected_group_list, @query.available_filters["member_of_group"][:values]
404 end
405
406 end
407
408 end
409
410 context "#statement" do
411 context "with 'member_of_group' filter" do
412 setup do
413 Group.destroy_all # No fixtures
414 @user_in_group = User.generate!
415 @second_user_in_group = User.generate!
416 @user_in_group2 = User.generate!
417 @user_not_in_group = User.generate!
418
419 @group = Group.generate!.reload
420 @group.users << @user_in_group
421 @group.users << @second_user_in_group
422
423 @group2 = Group.generate!.reload
424 @group2.users << @user_in_group2
425
426 end
427
428 should "search assigned to for users in the group" do
429 @query = Query.new(:name => '_')
430 @query.add_filter('member_of_group', '=', [@group.id.to_s])
431
432 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
433 assert_find_issues_with_query_is_successful @query
434 end
435
436 should "search not assigned to any group member (none)" do
437 @query = Query.new(:name => '_')
438 @query.add_filter('member_of_group', '!*', [''])
439
440 # Users not in a group
441 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
442 assert_find_issues_with_query_is_successful @query
443
444 end
445
446 should "search assigned to any group member (all)" do
447 @query = Query.new(:name => '_')
448 @query.add_filter('member_of_group', '*', [''])
449
450 # Only users in a group
451 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
452 assert_find_issues_with_query_is_successful @query
453
454 end
455 end
456 end
457
372 458 end
General Comments 0
You need to be logged in to leave comments. Login now