##// END OF EJS Templates
Adds members of subprojects in assignee and author filters (#10126)....
Jean-Philippe Lang -
r8627:2808adf99f44
parent child
Show More
@@ -1,62 +1,72
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Principal < ActiveRecord::Base
18 class Principal < ActiveRecord::Base
19 set_table_name "#{table_name_prefix}users#{table_name_suffix}"
19 set_table_name "#{table_name_prefix}users#{table_name_suffix}"
20
20
21 has_many :members, :foreign_key => 'user_id', :dependent => :destroy
21 has_many :members, :foreign_key => 'user_id', :dependent => :destroy
22 has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
22 has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
23 has_many :projects, :through => :memberships
23 has_many :projects, :through => :memberships
24 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
24 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
25
25
26 # Groups and active users
26 # Groups and active users
27 named_scope :active, :conditions => "#{Principal.table_name}.status = 1"
27 named_scope :active, :conditions => "#{Principal.table_name}.status = 1"
28
28
29 named_scope :like, lambda {|q|
29 named_scope :like, lambda {|q|
30 s = "%#{q.to_s.strip.downcase}%"
30 s = "%#{q.to_s.strip.downcase}%"
31 {:conditions => ["LOWER(login) LIKE :s OR LOWER(firstname) LIKE :s OR LOWER(lastname) LIKE :s OR LOWER(mail) LIKE :s", {:s => s}],
31 {:conditions => ["LOWER(login) LIKE :s OR LOWER(firstname) LIKE :s OR LOWER(lastname) LIKE :s OR LOWER(mail) LIKE :s", {:s => s}],
32 :order => 'type, login, lastname, firstname, mail'
32 :order => 'type, login, lastname, firstname, mail'
33 }
33 }
34 }
34 }
35
35
36 # Principals that are members of a collection of projects
37 named_scope :member_of, lambda {|projects|
38 if projects.empty?
39 {:conditions => "1=0"}
40 else
41 ids = projects.map(&:id)
42 {:conditions => ["#{Principal.table_name}.status = 1 AND #{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids]}
43 end
44 }
45
36 before_create :set_default_empty_values
46 before_create :set_default_empty_values
37
47
38 def name(formatter = nil)
48 def name(formatter = nil)
39 to_s
49 to_s
40 end
50 end
41
51
42 def <=>(principal)
52 def <=>(principal)
43 if self.class.name == principal.class.name
53 if self.class.name == principal.class.name
44 self.to_s.downcase <=> principal.to_s.downcase
54 self.to_s.downcase <=> principal.to_s.downcase
45 else
55 else
46 # groups after users
56 # groups after users
47 principal.class.name <=> self.class.name
57 principal.class.name <=> self.class.name
48 end
58 end
49 end
59 end
50
60
51 protected
61 protected
52
62
53 # Make sure we don't try to insert NULL values (see #4632)
63 # Make sure we don't try to insert NULL values (see #4632)
54 def set_default_empty_values
64 def set_default_empty_values
55 self.login ||= ''
65 self.login ||= ''
56 self.hashed_password ||= ''
66 self.hashed_password ||= ''
57 self.firstname ||= ''
67 self.firstname ||= ''
58 self.lastname ||= ''
68 self.lastname ||= ''
59 self.mail ||= ''
69 self.mail ||= ''
60 true
70 true
61 end
71 end
62 end
72 end
@@ -1,862 +1,865
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !@sortable.nil?
39 !@sortable.nil?
40 end
40 end
41
41
42 def sortable
42 def sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 end
44 end
45
45
46 def value(issue)
46 def value(issue)
47 issue.send name
47 issue.send name
48 end
48 end
49
49
50 def css_classes
50 def css_classes
51 name
51 name
52 end
52 end
53 end
53 end
54
54
55 class QueryCustomFieldColumn < QueryColumn
55 class QueryCustomFieldColumn < QueryColumn
56
56
57 def initialize(custom_field)
57 def initialize(custom_field)
58 self.name = "cf_#{custom_field.id}".to_sym
58 self.name = "cf_#{custom_field.id}".to_sym
59 self.sortable = custom_field.order_statement || false
59 self.sortable = custom_field.order_statement || false
60 if %w(list date bool int).include?(custom_field.field_format) && !custom_field.multiple?
60 if %w(list date bool int).include?(custom_field.field_format) && !custom_field.multiple?
61 self.groupable = custom_field.order_statement
61 self.groupable = custom_field.order_statement
62 end
62 end
63 self.groupable ||= false
63 self.groupable ||= false
64 @cf = custom_field
64 @cf = custom_field
65 end
65 end
66
66
67 def caption
67 def caption
68 @cf.name
68 @cf.name
69 end
69 end
70
70
71 def custom_field
71 def custom_field
72 @cf
72 @cf
73 end
73 end
74
74
75 def value(issue)
75 def value(issue)
76 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
76 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
77 cv.size > 1 ? cv : cv.first
77 cv.size > 1 ? cv : cv.first
78 end
78 end
79
79
80 def css_classes
80 def css_classes
81 @css_classes ||= "#{name} #{@cf.field_format}"
81 @css_classes ||= "#{name} #{@cf.field_format}"
82 end
82 end
83 end
83 end
84
84
85 class Query < ActiveRecord::Base
85 class Query < ActiveRecord::Base
86 class StatementInvalid < ::ActiveRecord::StatementInvalid
86 class StatementInvalid < ::ActiveRecord::StatementInvalid
87 end
87 end
88
88
89 belongs_to :project
89 belongs_to :project
90 belongs_to :user
90 belongs_to :user
91 serialize :filters
91 serialize :filters
92 serialize :column_names
92 serialize :column_names
93 serialize :sort_criteria, Array
93 serialize :sort_criteria, Array
94
94
95 attr_protected :project_id, :user_id
95 attr_protected :project_id, :user_id
96
96
97 validates_presence_of :name, :on => :save
97 validates_presence_of :name, :on => :save
98 validates_length_of :name, :maximum => 255
98 validates_length_of :name, :maximum => 255
99 validate :validate_query_filters
99 validate :validate_query_filters
100
100
101 @@operators = { "=" => :label_equals,
101 @@operators = { "=" => :label_equals,
102 "!" => :label_not_equals,
102 "!" => :label_not_equals,
103 "o" => :label_open_issues,
103 "o" => :label_open_issues,
104 "c" => :label_closed_issues,
104 "c" => :label_closed_issues,
105 "!*" => :label_none,
105 "!*" => :label_none,
106 "*" => :label_all,
106 "*" => :label_all,
107 ">=" => :label_greater_or_equal,
107 ">=" => :label_greater_or_equal,
108 "<=" => :label_less_or_equal,
108 "<=" => :label_less_or_equal,
109 "><" => :label_between,
109 "><" => :label_between,
110 "<t+" => :label_in_less_than,
110 "<t+" => :label_in_less_than,
111 ">t+" => :label_in_more_than,
111 ">t+" => :label_in_more_than,
112 "t+" => :label_in,
112 "t+" => :label_in,
113 "t" => :label_today,
113 "t" => :label_today,
114 "w" => :label_this_week,
114 "w" => :label_this_week,
115 ">t-" => :label_less_than_ago,
115 ">t-" => :label_less_than_ago,
116 "<t-" => :label_more_than_ago,
116 "<t-" => :label_more_than_ago,
117 "t-" => :label_ago,
117 "t-" => :label_ago,
118 "~" => :label_contains,
118 "~" => :label_contains,
119 "!~" => :label_not_contains }
119 "!~" => :label_not_contains }
120
120
121 cattr_reader :operators
121 cattr_reader :operators
122
122
123 @@operators_by_filter_type = { :list => [ "=", "!" ],
123 @@operators_by_filter_type = { :list => [ "=", "!" ],
124 :list_status => [ "o", "=", "!", "c", "*" ],
124 :list_status => [ "o", "=", "!", "c", "*" ],
125 :list_optional => [ "=", "!", "!*", "*" ],
125 :list_optional => [ "=", "!", "!*", "*" ],
126 :list_subprojects => [ "*", "!*", "=" ],
126 :list_subprojects => [ "*", "!*", "=" ],
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
129 :string => [ "=", "~", "!", "!~" ],
129 :string => [ "=", "~", "!", "!~" ],
130 :text => [ "~", "!~" ],
130 :text => [ "~", "!~" ],
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
133
133
134 cattr_reader :operators_by_filter_type
134 cattr_reader :operators_by_filter_type
135
135
136 @@available_columns = [
136 @@available_columns = [
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
153 ]
153 ]
154 cattr_reader :available_columns
154 cattr_reader :available_columns
155
155
156 named_scope :visible, lambda {|*args|
156 named_scope :visible, lambda {|*args|
157 user = args.shift || User.current
157 user = args.shift || User.current
158 base = Project.allowed_to_condition(user, :view_issues, *args)
158 base = Project.allowed_to_condition(user, :view_issues, *args)
159 user_id = user.logged? ? user.id : 0
159 user_id = user.logged? ? user.id : 0
160 {
160 {
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
162 :include => :project
162 :include => :project
163 }
163 }
164 }
164 }
165
165
166 def initialize(attributes=nil, *args)
166 def initialize(attributes=nil, *args)
167 super attributes
167 super attributes
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
169 @is_for_all = project.nil?
169 @is_for_all = project.nil?
170 end
170 end
171
171
172 def validate_query_filters
172 def validate_query_filters
173 filters.each_key do |field|
173 filters.each_key do |field|
174 if values_for(field)
174 if values_for(field)
175 case type_for(field)
175 case type_for(field)
176 when :integer
176 when :integer
177 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
177 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
178 when :float
178 when :float
179 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
179 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
180 when :date, :date_past
180 when :date, :date_past
181 case operator_for(field)
181 case operator_for(field)
182 when "=", ">=", "<=", "><"
182 when "=", ">=", "<=", "><"
183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
184 when ">t-", "<t-", "t-"
184 when ">t-", "<t-", "t-"
185 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
185 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
186 end
186 end
187 end
187 end
188 end
188 end
189
189
190 add_filter_error(field, :blank) unless
190 add_filter_error(field, :blank) unless
191 # filter requires one or more values
191 # filter requires one or more values
192 (values_for(field) and !values_for(field).first.blank?) or
192 (values_for(field) and !values_for(field).first.blank?) or
193 # filter doesn't require any value
193 # filter doesn't require any value
194 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
194 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
195 end if filters
195 end if filters
196 end
196 end
197
197
198 def add_filter_error(field, message)
198 def add_filter_error(field, message)
199 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
199 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
200 errors.add(:base, m)
200 errors.add(:base, m)
201 end
201 end
202
202
203 # Returns true if the query is visible to +user+ or the current user.
203 # Returns true if the query is visible to +user+ or the current user.
204 def visible?(user=User.current)
204 def visible?(user=User.current)
205 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
205 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
206 end
206 end
207
207
208 def editable_by?(user)
208 def editable_by?(user)
209 return false unless user
209 return false unless user
210 # Admin can edit them all and regular users can edit their private queries
210 # Admin can edit them all and regular users can edit their private queries
211 return true if user.admin? || (!is_public && self.user_id == user.id)
211 return true if user.admin? || (!is_public && self.user_id == user.id)
212 # Members can not edit public queries that are for all project (only admin is allowed to)
212 # Members can not edit public queries that are for all project (only admin is allowed to)
213 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
213 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
214 end
214 end
215
215
216 def available_filters
216 def available_filters
217 return @available_filters if @available_filters
217 return @available_filters if @available_filters
218
218
219 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
219 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
220
220
221 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
221 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
222 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
222 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
223 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
223 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
224 "subject" => { :type => :text, :order => 8 },
224 "subject" => { :type => :text, :order => 8 },
225 "created_on" => { :type => :date_past, :order => 9 },
225 "created_on" => { :type => :date_past, :order => 9 },
226 "updated_on" => { :type => :date_past, :order => 10 },
226 "updated_on" => { :type => :date_past, :order => 10 },
227 "start_date" => { :type => :date, :order => 11 },
227 "start_date" => { :type => :date, :order => 11 },
228 "due_date" => { :type => :date, :order => 12 },
228 "due_date" => { :type => :date, :order => 12 },
229 "estimated_hours" => { :type => :float, :order => 13 },
229 "estimated_hours" => { :type => :float, :order => 13 },
230 "done_ratio" => { :type => :integer, :order => 14 }}
230 "done_ratio" => { :type => :integer, :order => 14 }}
231
231
232 principals = []
232 principals = []
233 if project
233 if project
234 principals += project.principals.sort
234 principals += project.principals.sort
235 unless project.leaf?
236 subprojects = project.descendants.visible.all
237 if subprojects.any?
238 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
239 principals += Principal.member_of(subprojects)
240 end
241 end
235 else
242 else
236 all_projects = Project.visible.all
243 all_projects = Project.visible.all
237 if all_projects.any?
244 if all_projects.any?
238 # members of visible projects
245 # members of visible projects
239 principals += Principal.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort
246 principals += Principal.member_of(all_projects)
240
247
241 # project filter
248 # project filter
242 project_values = []
249 project_values = []
243 if User.current.logged? && User.current.memberships.any?
250 if User.current.logged? && User.current.memberships.any?
244 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
251 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
245 end
252 end
246 Project.project_tree(all_projects) do |p, level|
253 Project.project_tree(all_projects) do |p, level|
247 prefix = (level > 0 ? ('--' * level + ' ') : '')
254 prefix = (level > 0 ? ('--' * level + ' ') : '')
248 project_values << ["#{prefix}#{p.name}", p.id.to_s]
255 project_values << ["#{prefix}#{p.name}", p.id.to_s]
249 end
256 end
250 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
257 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
251 end
258 end
252 end
259 end
260 principals.uniq!
261 principals.sort!
253 users = principals.select {|p| p.is_a?(User)}
262 users = principals.select {|p| p.is_a?(User)}
254
263
255 assigned_to_values = []
264 assigned_to_values = []
256 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
265 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
257 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
266 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
258 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
267 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
259
268
260 author_values = []
269 author_values = []
261 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
270 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
262 author_values += users.collect{|s| [s.name, s.id.to_s] }
271 author_values += users.collect{|s| [s.name, s.id.to_s] }
263 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
272 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
264
273
265 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
274 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
266 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
275 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
267
276
268 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
277 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
269 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
278 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
270
279
271 if User.current.logged?
280 if User.current.logged?
272 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
281 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
273 end
282 end
274
283
275 if project
284 if project
276 # project specific filters
285 # project specific filters
277 categories = project.issue_categories.all
286 categories = project.issue_categories.all
278 unless categories.empty?
287 unless categories.empty?
279 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
288 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
280 end
289 end
281 versions = project.shared_versions.all
290 versions = project.shared_versions.all
282 unless versions.empty?
291 unless versions.empty?
283 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
292 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
284 end
293 end
285 unless project.leaf?
286 subprojects = project.descendants.visible.all
287 unless subprojects.empty?
288 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
289 end
290 end
291 add_custom_fields_filters(project.all_issue_custom_fields)
294 add_custom_fields_filters(project.all_issue_custom_fields)
292 else
295 else
293 # global filters for cross project issue list
296 # global filters for cross project issue list
294 system_shared_versions = Version.visible.find_all_by_sharing('system')
297 system_shared_versions = Version.visible.find_all_by_sharing('system')
295 unless system_shared_versions.empty?
298 unless system_shared_versions.empty?
296 @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] } }
299 @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] } }
297 end
300 end
298 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
301 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
299 end
302 end
300 @available_filters
303 @available_filters
301 end
304 end
302
305
303 def add_filter(field, operator, values)
306 def add_filter(field, operator, values)
304 # values must be an array
307 # values must be an array
305 return unless values.nil? || values.is_a?(Array)
308 return unless values.nil? || values.is_a?(Array)
306 # check if field is defined as an available filter
309 # check if field is defined as an available filter
307 if available_filters.has_key? field
310 if available_filters.has_key? field
308 filter_options = available_filters[field]
311 filter_options = available_filters[field]
309 # check if operator is allowed for that filter
312 # check if operator is allowed for that filter
310 #if @@operators_by_filter_type[filter_options[:type]].include? operator
313 #if @@operators_by_filter_type[filter_options[:type]].include? operator
311 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
314 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
312 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
315 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
313 #end
316 #end
314 filters[field] = {:operator => operator, :values => (values || [''])}
317 filters[field] = {:operator => operator, :values => (values || [''])}
315 end
318 end
316 end
319 end
317
320
318 def add_short_filter(field, expression)
321 def add_short_filter(field, expression)
319 return unless expression && available_filters.has_key?(field)
322 return unless expression && available_filters.has_key?(field)
320 field_type = available_filters[field][:type]
323 field_type = available_filters[field][:type]
321 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
324 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
322 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
325 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
323 add_filter field, operator, $1.present? ? $1.split('|') : ['']
326 add_filter field, operator, $1.present? ? $1.split('|') : ['']
324 end || add_filter(field, '=', expression.split('|'))
327 end || add_filter(field, '=', expression.split('|'))
325 end
328 end
326
329
327 # Add multiple filters using +add_filter+
330 # Add multiple filters using +add_filter+
328 def add_filters(fields, operators, values)
331 def add_filters(fields, operators, values)
329 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
332 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
330 fields.each do |field|
333 fields.each do |field|
331 add_filter(field, operators[field], values && values[field])
334 add_filter(field, operators[field], values && values[field])
332 end
335 end
333 end
336 end
334 end
337 end
335
338
336 def has_filter?(field)
339 def has_filter?(field)
337 filters and filters[field]
340 filters and filters[field]
338 end
341 end
339
342
340 def type_for(field)
343 def type_for(field)
341 available_filters[field][:type] if available_filters.has_key?(field)
344 available_filters[field][:type] if available_filters.has_key?(field)
342 end
345 end
343
346
344 def operator_for(field)
347 def operator_for(field)
345 has_filter?(field) ? filters[field][:operator] : nil
348 has_filter?(field) ? filters[field][:operator] : nil
346 end
349 end
347
350
348 def values_for(field)
351 def values_for(field)
349 has_filter?(field) ? filters[field][:values] : nil
352 has_filter?(field) ? filters[field][:values] : nil
350 end
353 end
351
354
352 def value_for(field, index=0)
355 def value_for(field, index=0)
353 (values_for(field) || [])[index]
356 (values_for(field) || [])[index]
354 end
357 end
355
358
356 def label_for(field)
359 def label_for(field)
357 label = available_filters[field][:name] if available_filters.has_key?(field)
360 label = available_filters[field][:name] if available_filters.has_key?(field)
358 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
361 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
359 end
362 end
360
363
361 def available_columns
364 def available_columns
362 return @available_columns if @available_columns
365 return @available_columns if @available_columns
363 @available_columns = ::Query.available_columns.dup
366 @available_columns = ::Query.available_columns.dup
364 @available_columns += (project ?
367 @available_columns += (project ?
365 project.all_issue_custom_fields :
368 project.all_issue_custom_fields :
366 IssueCustomField.find(:all)
369 IssueCustomField.find(:all)
367 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
370 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
368
371
369 if User.current.allowed_to?(:view_time_entries, project, :global => true)
372 if User.current.allowed_to?(:view_time_entries, project, :global => true)
370 index = nil
373 index = nil
371 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
374 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
372 index = (index ? index + 1 : -1)
375 index = (index ? index + 1 : -1)
373 # insert the column after estimated_hours or at the end
376 # insert the column after estimated_hours or at the end
374 @available_columns.insert index, QueryColumn.new(:spent_hours,
377 @available_columns.insert index, QueryColumn.new(:spent_hours,
375 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
378 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
376 :default_order => 'desc',
379 :default_order => 'desc',
377 :caption => :label_spent_time
380 :caption => :label_spent_time
378 )
381 )
379 end
382 end
380 @available_columns
383 @available_columns
381 end
384 end
382
385
383 def self.available_columns=(v)
386 def self.available_columns=(v)
384 self.available_columns = (v)
387 self.available_columns = (v)
385 end
388 end
386
389
387 def self.add_available_column(column)
390 def self.add_available_column(column)
388 self.available_columns << (column) if column.is_a?(QueryColumn)
391 self.available_columns << (column) if column.is_a?(QueryColumn)
389 end
392 end
390
393
391 # Returns an array of columns that can be used to group the results
394 # Returns an array of columns that can be used to group the results
392 def groupable_columns
395 def groupable_columns
393 available_columns.select {|c| c.groupable}
396 available_columns.select {|c| c.groupable}
394 end
397 end
395
398
396 # Returns a Hash of columns and the key for sorting
399 # Returns a Hash of columns and the key for sorting
397 def sortable_columns
400 def sortable_columns
398 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
401 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
399 h[column.name.to_s] = column.sortable
402 h[column.name.to_s] = column.sortable
400 h
403 h
401 })
404 })
402 end
405 end
403
406
404 def columns
407 def columns
405 # preserve the column_names order
408 # preserve the column_names order
406 (has_default_columns? ? default_columns_names : column_names).collect do |name|
409 (has_default_columns? ? default_columns_names : column_names).collect do |name|
407 available_columns.find { |col| col.name == name }
410 available_columns.find { |col| col.name == name }
408 end.compact
411 end.compact
409 end
412 end
410
413
411 def default_columns_names
414 def default_columns_names
412 @default_columns_names ||= begin
415 @default_columns_names ||= begin
413 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
416 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
414
417
415 project.present? ? default_columns : [:project] | default_columns
418 project.present? ? default_columns : [:project] | default_columns
416 end
419 end
417 end
420 end
418
421
419 def column_names=(names)
422 def column_names=(names)
420 if names
423 if names
421 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
424 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
422 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
425 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
423 # Set column_names to nil if default columns
426 # Set column_names to nil if default columns
424 if names == default_columns_names
427 if names == default_columns_names
425 names = nil
428 names = nil
426 end
429 end
427 end
430 end
428 write_attribute(:column_names, names)
431 write_attribute(:column_names, names)
429 end
432 end
430
433
431 def has_column?(column)
434 def has_column?(column)
432 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
435 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
433 end
436 end
434
437
435 def has_default_columns?
438 def has_default_columns?
436 column_names.nil? || column_names.empty?
439 column_names.nil? || column_names.empty?
437 end
440 end
438
441
439 def sort_criteria=(arg)
442 def sort_criteria=(arg)
440 c = []
443 c = []
441 if arg.is_a?(Hash)
444 if arg.is_a?(Hash)
442 arg = arg.keys.sort.collect {|k| arg[k]}
445 arg = arg.keys.sort.collect {|k| arg[k]}
443 end
446 end
444 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
447 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
445 write_attribute(:sort_criteria, c)
448 write_attribute(:sort_criteria, c)
446 end
449 end
447
450
448 def sort_criteria
451 def sort_criteria
449 read_attribute(:sort_criteria) || []
452 read_attribute(:sort_criteria) || []
450 end
453 end
451
454
452 def sort_criteria_key(arg)
455 def sort_criteria_key(arg)
453 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
456 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
454 end
457 end
455
458
456 def sort_criteria_order(arg)
459 def sort_criteria_order(arg)
457 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
460 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
458 end
461 end
459
462
460 # Returns the SQL sort order that should be prepended for grouping
463 # Returns the SQL sort order that should be prepended for grouping
461 def group_by_sort_order
464 def group_by_sort_order
462 if grouped? && (column = group_by_column)
465 if grouped? && (column = group_by_column)
463 column.sortable.is_a?(Array) ?
466 column.sortable.is_a?(Array) ?
464 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
467 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
465 "#{column.sortable} #{column.default_order}"
468 "#{column.sortable} #{column.default_order}"
466 end
469 end
467 end
470 end
468
471
469 # Returns true if the query is a grouped query
472 # Returns true if the query is a grouped query
470 def grouped?
473 def grouped?
471 !group_by_column.nil?
474 !group_by_column.nil?
472 end
475 end
473
476
474 def group_by_column
477 def group_by_column
475 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
478 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
476 end
479 end
477
480
478 def group_by_statement
481 def group_by_statement
479 group_by_column.try(:groupable)
482 group_by_column.try(:groupable)
480 end
483 end
481
484
482 def project_statement
485 def project_statement
483 project_clauses = []
486 project_clauses = []
484 if project && !project.descendants.active.empty?
487 if project && !project.descendants.active.empty?
485 ids = [project.id]
488 ids = [project.id]
486 if has_filter?("subproject_id")
489 if has_filter?("subproject_id")
487 case operator_for("subproject_id")
490 case operator_for("subproject_id")
488 when '='
491 when '='
489 # include the selected subprojects
492 # include the selected subprojects
490 ids += values_for("subproject_id").each(&:to_i)
493 ids += values_for("subproject_id").each(&:to_i)
491 when '!*'
494 when '!*'
492 # main project only
495 # main project only
493 else
496 else
494 # all subprojects
497 # all subprojects
495 ids += project.descendants.collect(&:id)
498 ids += project.descendants.collect(&:id)
496 end
499 end
497 elsif Setting.display_subprojects_issues?
500 elsif Setting.display_subprojects_issues?
498 ids += project.descendants.collect(&:id)
501 ids += project.descendants.collect(&:id)
499 end
502 end
500 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
503 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
501 elsif project
504 elsif project
502 project_clauses << "#{Project.table_name}.id = %d" % project.id
505 project_clauses << "#{Project.table_name}.id = %d" % project.id
503 end
506 end
504 project_clauses.any? ? project_clauses.join(' AND ') : nil
507 project_clauses.any? ? project_clauses.join(' AND ') : nil
505 end
508 end
506
509
507 def statement
510 def statement
508 # filters clauses
511 # filters clauses
509 filters_clauses = []
512 filters_clauses = []
510 filters.each_key do |field|
513 filters.each_key do |field|
511 next if field == "subproject_id"
514 next if field == "subproject_id"
512 v = values_for(field).clone
515 v = values_for(field).clone
513 next unless v and !v.empty?
516 next unless v and !v.empty?
514 operator = operator_for(field)
517 operator = operator_for(field)
515
518
516 # "me" value subsitution
519 # "me" value subsitution
517 if %w(assigned_to_id author_id watcher_id).include?(field)
520 if %w(assigned_to_id author_id watcher_id).include?(field)
518 if v.delete("me")
521 if v.delete("me")
519 if User.current.logged?
522 if User.current.logged?
520 v.push(User.current.id.to_s)
523 v.push(User.current.id.to_s)
521 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
524 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
522 else
525 else
523 v.push("0")
526 v.push("0")
524 end
527 end
525 end
528 end
526 end
529 end
527
530
528 if field == 'project_id'
531 if field == 'project_id'
529 if v.delete('mine')
532 if v.delete('mine')
530 v += User.current.memberships.map(&:project_id).map(&:to_s)
533 v += User.current.memberships.map(&:project_id).map(&:to_s)
531 end
534 end
532 end
535 end
533
536
534 if field =~ /^cf_(\d+)$/
537 if field =~ /^cf_(\d+)$/
535 # custom field
538 # custom field
536 filters_clauses << sql_for_custom_field(field, operator, v, $1)
539 filters_clauses << sql_for_custom_field(field, operator, v, $1)
537 elsif respond_to?("sql_for_#{field}_field")
540 elsif respond_to?("sql_for_#{field}_field")
538 # specific statement
541 # specific statement
539 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
542 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
540 else
543 else
541 # regular field
544 # regular field
542 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
545 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
543 end
546 end
544 end if filters and valid?
547 end if filters and valid?
545
548
546 filters_clauses << project_statement
549 filters_clauses << project_statement
547 filters_clauses.reject!(&:blank?)
550 filters_clauses.reject!(&:blank?)
548
551
549 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
552 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
550 end
553 end
551
554
552 # Returns the issue count
555 # Returns the issue count
553 def issue_count
556 def issue_count
554 Issue.visible.count(:include => [:status, :project], :conditions => statement)
557 Issue.visible.count(:include => [:status, :project], :conditions => statement)
555 rescue ::ActiveRecord::StatementInvalid => e
558 rescue ::ActiveRecord::StatementInvalid => e
556 raise StatementInvalid.new(e.message)
559 raise StatementInvalid.new(e.message)
557 end
560 end
558
561
559 # Returns the issue count by group or nil if query is not grouped
562 # Returns the issue count by group or nil if query is not grouped
560 def issue_count_by_group
563 def issue_count_by_group
561 r = nil
564 r = nil
562 if grouped?
565 if grouped?
563 begin
566 begin
564 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
567 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
565 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
568 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
566 rescue ActiveRecord::RecordNotFound
569 rescue ActiveRecord::RecordNotFound
567 r = {nil => issue_count}
570 r = {nil => issue_count}
568 end
571 end
569 c = group_by_column
572 c = group_by_column
570 if c.is_a?(QueryCustomFieldColumn)
573 if c.is_a?(QueryCustomFieldColumn)
571 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
574 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
572 end
575 end
573 end
576 end
574 r
577 r
575 rescue ::ActiveRecord::StatementInvalid => e
578 rescue ::ActiveRecord::StatementInvalid => e
576 raise StatementInvalid.new(e.message)
579 raise StatementInvalid.new(e.message)
577 end
580 end
578
581
579 # Returns the issues
582 # Returns the issues
580 # Valid options are :order, :offset, :limit, :include, :conditions
583 # Valid options are :order, :offset, :limit, :include, :conditions
581 def issues(options={})
584 def issues(options={})
582 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
585 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
583 order_option = nil if order_option.blank?
586 order_option = nil if order_option.blank?
584
587
585 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
588 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
586
589
587 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
590 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
588 :conditions => statement,
591 :conditions => statement,
589 :order => order_option,
592 :order => order_option,
590 :joins => joins,
593 :joins => joins,
591 :limit => options[:limit],
594 :limit => options[:limit],
592 :offset => options[:offset]
595 :offset => options[:offset]
593
596
594 if has_column?(:spent_hours)
597 if has_column?(:spent_hours)
595 Issue.load_visible_spent_hours(issues)
598 Issue.load_visible_spent_hours(issues)
596 end
599 end
597 issues
600 issues
598 rescue ::ActiveRecord::StatementInvalid => e
601 rescue ::ActiveRecord::StatementInvalid => e
599 raise StatementInvalid.new(e.message)
602 raise StatementInvalid.new(e.message)
600 end
603 end
601
604
602 # Returns the issues ids
605 # Returns the issues ids
603 def issue_ids(options={})
606 def issue_ids(options={})
604 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
607 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
605 order_option = nil if order_option.blank?
608 order_option = nil if order_option.blank?
606
609
607 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
610 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
608
611
609 Issue.visible.scoped(:conditions => options[:conditions]).find_ids :include => ([:status, :project] + (options[:include] || [])).uniq,
612 Issue.visible.scoped(:conditions => options[:conditions]).find_ids :include => ([:status, :project] + (options[:include] || [])).uniq,
610 :conditions => statement,
613 :conditions => statement,
611 :order => order_option,
614 :order => order_option,
612 :joins => joins,
615 :joins => joins,
613 :limit => options[:limit],
616 :limit => options[:limit],
614 :offset => options[:offset]
617 :offset => options[:offset]
615 rescue ::ActiveRecord::StatementInvalid => e
618 rescue ::ActiveRecord::StatementInvalid => e
616 raise StatementInvalid.new(e.message)
619 raise StatementInvalid.new(e.message)
617 end
620 end
618
621
619 # Returns the journals
622 # Returns the journals
620 # Valid options are :order, :offset, :limit
623 # Valid options are :order, :offset, :limit
621 def journals(options={})
624 def journals(options={})
622 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
625 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
623 :conditions => statement,
626 :conditions => statement,
624 :order => options[:order],
627 :order => options[:order],
625 :limit => options[:limit],
628 :limit => options[:limit],
626 :offset => options[:offset]
629 :offset => options[:offset]
627 rescue ::ActiveRecord::StatementInvalid => e
630 rescue ::ActiveRecord::StatementInvalid => e
628 raise StatementInvalid.new(e.message)
631 raise StatementInvalid.new(e.message)
629 end
632 end
630
633
631 # Returns the versions
634 # Returns the versions
632 # Valid options are :conditions
635 # Valid options are :conditions
633 def versions(options={})
636 def versions(options={})
634 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
637 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
635 rescue ::ActiveRecord::StatementInvalid => e
638 rescue ::ActiveRecord::StatementInvalid => e
636 raise StatementInvalid.new(e.message)
639 raise StatementInvalid.new(e.message)
637 end
640 end
638
641
639 def sql_for_watcher_id_field(field, operator, value)
642 def sql_for_watcher_id_field(field, operator, value)
640 db_table = Watcher.table_name
643 db_table = Watcher.table_name
641 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
644 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
642 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
645 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
643 end
646 end
644
647
645 def sql_for_member_of_group_field(field, operator, value)
648 def sql_for_member_of_group_field(field, operator, value)
646 if operator == '*' # Any group
649 if operator == '*' # Any group
647 groups = Group.all
650 groups = Group.all
648 operator = '=' # Override the operator since we want to find by assigned_to
651 operator = '=' # Override the operator since we want to find by assigned_to
649 elsif operator == "!*"
652 elsif operator == "!*"
650 groups = Group.all
653 groups = Group.all
651 operator = '!' # Override the operator since we want to find by assigned_to
654 operator = '!' # Override the operator since we want to find by assigned_to
652 else
655 else
653 groups = Group.find_all_by_id(value)
656 groups = Group.find_all_by_id(value)
654 end
657 end
655 groups ||= []
658 groups ||= []
656
659
657 members_of_groups = groups.inject([]) {|user_ids, group|
660 members_of_groups = groups.inject([]) {|user_ids, group|
658 if group && group.user_ids.present?
661 if group && group.user_ids.present?
659 user_ids << group.user_ids
662 user_ids << group.user_ids
660 end
663 end
661 user_ids.flatten.uniq.compact
664 user_ids.flatten.uniq.compact
662 }.sort.collect(&:to_s)
665 }.sort.collect(&:to_s)
663
666
664 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
667 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
665 end
668 end
666
669
667 def sql_for_assigned_to_role_field(field, operator, value)
670 def sql_for_assigned_to_role_field(field, operator, value)
668 case operator
671 case operator
669 when "*", "!*" # Member / Not member
672 when "*", "!*" # Member / Not member
670 sw = operator == "!*" ? 'NOT' : ''
673 sw = operator == "!*" ? 'NOT' : ''
671 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
674 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
672 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
675 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
673 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
676 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
674 when "=", "!"
677 when "=", "!"
675 role_cond = value.any? ?
678 role_cond = value.any? ?
676 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
679 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
677 "1=0"
680 "1=0"
678
681
679 sw = operator == "!" ? 'NOT' : ''
682 sw = operator == "!" ? 'NOT' : ''
680 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
683 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
681 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
684 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
682 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
685 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
683 end
686 end
684 end
687 end
685
688
686 private
689 private
687
690
688 def sql_for_custom_field(field, operator, value, custom_field_id)
691 def sql_for_custom_field(field, operator, value, custom_field_id)
689 db_table = CustomValue.table_name
692 db_table = CustomValue.table_name
690 db_field = 'value'
693 db_field = 'value'
691 filter = @available_filters[field]
694 filter = @available_filters[field]
692 if filter && filter[:format] == 'user'
695 if filter && filter[:format] == 'user'
693 if value.delete('me')
696 if value.delete('me')
694 value.push User.current.id.to_s
697 value.push User.current.id.to_s
695 end
698 end
696 end
699 end
697 not_in = nil
700 not_in = nil
698 if operator == '!'
701 if operator == '!'
699 # Makes ! operator work for custom fields with multiple values
702 # Makes ! operator work for custom fields with multiple values
700 operator = '='
703 operator = '='
701 not_in = 'NOT'
704 not_in = 'NOT'
702 end
705 end
703 "#{Issue.table_name}.id #{not_in} 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=#{custom_field_id} WHERE " +
706 "#{Issue.table_name}.id #{not_in} 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=#{custom_field_id} WHERE " +
704 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
707 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
705 end
708 end
706
709
707 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
710 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
708 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
711 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
709 sql = ''
712 sql = ''
710 case operator
713 case operator
711 when "="
714 when "="
712 if value.any?
715 if value.any?
713 case type_for(field)
716 case type_for(field)
714 when :date, :date_past
717 when :date, :date_past
715 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
718 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
716 when :integer
719 when :integer
717 if is_custom_filter
720 if is_custom_filter
718 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
721 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
719 else
722 else
720 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
723 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
721 end
724 end
722 when :float
725 when :float
723 if is_custom_filter
726 if is_custom_filter
724 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
727 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
725 else
728 else
726 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
729 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
727 end
730 end
728 else
731 else
729 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
732 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
730 end
733 end
731 else
734 else
732 # IN an empty set
735 # IN an empty set
733 sql = "1=0"
736 sql = "1=0"
734 end
737 end
735 when "!"
738 when "!"
736 if value.any?
739 if value.any?
737 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
740 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
738 else
741 else
739 # NOT IN an empty set
742 # NOT IN an empty set
740 sql = "1=1"
743 sql = "1=1"
741 end
744 end
742 when "!*"
745 when "!*"
743 sql = "#{db_table}.#{db_field} IS NULL"
746 sql = "#{db_table}.#{db_field} IS NULL"
744 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
747 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
745 when "*"
748 when "*"
746 sql = "#{db_table}.#{db_field} IS NOT NULL"
749 sql = "#{db_table}.#{db_field} IS NOT NULL"
747 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
750 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
748 when ">="
751 when ">="
749 if [:date, :date_past].include?(type_for(field))
752 if [:date, :date_past].include?(type_for(field))
750 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
753 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
751 else
754 else
752 if is_custom_filter
755 if is_custom_filter
753 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
756 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
754 else
757 else
755 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
758 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
756 end
759 end
757 end
760 end
758 when "<="
761 when "<="
759 if [:date, :date_past].include?(type_for(field))
762 if [:date, :date_past].include?(type_for(field))
760 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
763 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
761 else
764 else
762 if is_custom_filter
765 if is_custom_filter
763 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
766 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
764 else
767 else
765 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
768 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
766 end
769 end
767 end
770 end
768 when "><"
771 when "><"
769 if [:date, :date_past].include?(type_for(field))
772 if [:date, :date_past].include?(type_for(field))
770 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
773 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
771 else
774 else
772 if is_custom_filter
775 if is_custom_filter
773 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
776 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
774 else
777 else
775 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
778 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
776 end
779 end
777 end
780 end
778 when "o"
781 when "o"
779 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
782 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
780 when "c"
783 when "c"
781 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
784 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
782 when ">t-"
785 when ">t-"
783 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
786 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
784 when "<t-"
787 when "<t-"
785 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
788 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
786 when "t-"
789 when "t-"
787 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
790 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
788 when ">t+"
791 when ">t+"
789 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
792 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
790 when "<t+"
793 when "<t+"
791 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
794 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
792 when "t+"
795 when "t+"
793 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
796 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
794 when "t"
797 when "t"
795 sql = relative_date_clause(db_table, db_field, 0, 0)
798 sql = relative_date_clause(db_table, db_field, 0, 0)
796 when "w"
799 when "w"
797 first_day_of_week = l(:general_first_day_of_week).to_i
800 first_day_of_week = l(:general_first_day_of_week).to_i
798 day_of_week = Date.today.cwday
801 day_of_week = Date.today.cwday
799 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
802 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
800 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
803 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
801 when "~"
804 when "~"
802 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
805 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
803 when "!~"
806 when "!~"
804 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
807 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
805 else
808 else
806 raise "Unknown query operator #{operator}"
809 raise "Unknown query operator #{operator}"
807 end
810 end
808
811
809 return sql
812 return sql
810 end
813 end
811
814
812 def add_custom_fields_filters(custom_fields)
815 def add_custom_fields_filters(custom_fields)
813 @available_filters ||= {}
816 @available_filters ||= {}
814
817
815 custom_fields.select(&:is_filter?).each do |field|
818 custom_fields.select(&:is_filter?).each do |field|
816 case field.field_format
819 case field.field_format
817 when "text"
820 when "text"
818 options = { :type => :text, :order => 20 }
821 options = { :type => :text, :order => 20 }
819 when "list"
822 when "list"
820 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
823 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
821 when "date"
824 when "date"
822 options = { :type => :date, :order => 20 }
825 options = { :type => :date, :order => 20 }
823 when "bool"
826 when "bool"
824 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
827 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
825 when "int"
828 when "int"
826 options = { :type => :integer, :order => 20 }
829 options = { :type => :integer, :order => 20 }
827 when "float"
830 when "float"
828 options = { :type => :float, :order => 20 }
831 options = { :type => :float, :order => 20 }
829 when "user", "version"
832 when "user", "version"
830 next unless project
833 next unless project
831 values = field.possible_values_options(project)
834 values = field.possible_values_options(project)
832 if User.current.logged? && field.field_format == 'user'
835 if User.current.logged? && field.field_format == 'user'
833 values.unshift ["<< #{l(:label_me)} >>", "me"]
836 values.unshift ["<< #{l(:label_me)} >>", "me"]
834 end
837 end
835 options = { :type => :list_optional, :values => values, :order => 20}
838 options = { :type => :list_optional, :values => values, :order => 20}
836 else
839 else
837 options = { :type => :string, :order => 20 }
840 options = { :type => :string, :order => 20 }
838 end
841 end
839 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name, :format => field.field_format })
842 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name, :format => field.field_format })
840 end
843 end
841 end
844 end
842
845
843 # Returns a SQL clause for a date or datetime field.
846 # Returns a SQL clause for a date or datetime field.
844 def date_clause(table, field, from, to)
847 def date_clause(table, field, from, to)
845 s = []
848 s = []
846 if from
849 if from
847 from_yesterday = from - 1
850 from_yesterday = from - 1
848 from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day)
851 from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day)
849 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)])
852 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)])
850 end
853 end
851 if to
854 if to
852 to_utc = Time.gm(to.year, to.month, to.day)
855 to_utc = Time.gm(to.year, to.month, to.day)
853 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)])
856 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)])
854 end
857 end
855 s.join(' AND ')
858 s.join(' AND ')
856 end
859 end
857
860
858 # Returns a SQL clause for a date or datetime field using relative dates.
861 # Returns a SQL clause for a date or datetime field using relative dates.
859 def relative_date_clause(table, field, days_from, days_to)
862 def relative_date_clause(table, field, days_from, days_to)
860 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
863 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
861 end
864 end
862 end
865 end
@@ -1,74 +1,79
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class PrincipalTest < ActiveSupport::TestCase
20 class PrincipalTest < ActiveSupport::TestCase
21
21
22 def test_active_scope_should_return_groups_and_active_users
22 def test_active_scope_should_return_groups_and_active_users
23 result = Principal.active.all
23 result = Principal.active.all
24 assert_include Group.first, result
24 assert_include Group.first, result
25 assert_not_nil result.detect {|p| p.is_a?(User)}
25 assert_not_nil result.detect {|p| p.is_a?(User)}
26 assert_nil result.detect {|p| p.is_a?(User) && !p.active?}
26 assert_nil result.detect {|p| p.is_a?(User) && !p.active?}
27 assert_nil result.detect {|p| p.is_a?(AnonymousUser)}
27 assert_nil result.detect {|p| p.is_a?(AnonymousUser)}
28 end
28 end
29
29
30 def test_member_of_scope_should_return_the_union_of_all_members
31 projects = Project.find_all_by_id(1, 2)
32 assert_equal projects.map(&:principals).flatten.sort, Principal.member_of(projects).sort
33 end
34
30 context "#like" do
35 context "#like" do
31 setup do
36 setup do
32 Principal.generate!(:login => 'login')
37 Principal.generate!(:login => 'login')
33 Principal.generate!(:login => 'login2')
38 Principal.generate!(:login => 'login2')
34
39
35 Principal.generate!(:firstname => 'firstname')
40 Principal.generate!(:firstname => 'firstname')
36 Principal.generate!(:firstname => 'firstname2')
41 Principal.generate!(:firstname => 'firstname2')
37
42
38 Principal.generate!(:lastname => 'lastname')
43 Principal.generate!(:lastname => 'lastname')
39 Principal.generate!(:lastname => 'lastname2')
44 Principal.generate!(:lastname => 'lastname2')
40
45
41 Principal.generate!(:mail => 'mail@example.com')
46 Principal.generate!(:mail => 'mail@example.com')
42 Principal.generate!(:mail => 'mail2@example.com')
47 Principal.generate!(:mail => 'mail2@example.com')
43 end
48 end
44
49
45 should "search login" do
50 should "search login" do
46 results = Principal.like('login')
51 results = Principal.like('login')
47
52
48 assert_equal 2, results.count
53 assert_equal 2, results.count
49 assert results.all? {|u| u.login.match(/login/) }
54 assert results.all? {|u| u.login.match(/login/) }
50 end
55 end
51
56
52 should "search firstname" do
57 should "search firstname" do
53 results = Principal.like('firstname')
58 results = Principal.like('firstname')
54
59
55 assert_equal 2, results.count
60 assert_equal 2, results.count
56 assert results.all? {|u| u.firstname.match(/firstname/) }
61 assert results.all? {|u| u.firstname.match(/firstname/) }
57 end
62 end
58
63
59 should "search lastname" do
64 should "search lastname" do
60 results = Principal.like('lastname')
65 results = Principal.like('lastname')
61
66
62 assert_equal 2, results.count
67 assert_equal 2, results.count
63 assert results.all? {|u| u.lastname.match(/lastname/) }
68 assert results.all? {|u| u.lastname.match(/lastname/) }
64 end
69 end
65
70
66 should "search mail" do
71 should "search mail" do
67 results = Principal.like('mail')
72 results = Principal.like('mail')
68
73
69 assert_equal 2, results.count
74 assert_equal 2, results.count
70 assert results.all? {|u| u.mail.match(/mail/) }
75 assert results.all? {|u| u.mail.match(/mail/) }
71 end
76 end
72 end
77 end
73
78
74 end
79 end
@@ -1,960 +1,973
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 fixtures :projects, :enabled_modules, :users, :members,
21 fixtures :projects, :enabled_modules, :users, :members,
22 :member_roles, :roles, :trackers, :issue_statuses,
22 :member_roles, :roles, :trackers, :issue_statuses,
23 :issue_categories, :enumerations, :issues,
23 :issue_categories, :enumerations, :issues,
24 :watchers, :custom_fields, :custom_values, :versions,
24 :watchers, :custom_fields, :custom_values, :versions,
25 :queries,
25 :queries,
26 :projects_trackers
26 :projects_trackers
27
27
28 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
28 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
29 query = Query.new(:project => nil, :name => '_')
29 query = Query.new(:project => nil, :name => '_')
30 assert query.available_filters.has_key?('cf_1')
30 assert query.available_filters.has_key?('cf_1')
31 assert !query.available_filters.has_key?('cf_3')
31 assert !query.available_filters.has_key?('cf_3')
32 end
32 end
33
33
34 def test_system_shared_versions_should_be_available_in_global_queries
34 def test_system_shared_versions_should_be_available_in_global_queries
35 Version.find(2).update_attribute :sharing, 'system'
35 Version.find(2).update_attribute :sharing, 'system'
36 query = Query.new(:project => nil, :name => '_')
36 query = Query.new(:project => nil, :name => '_')
37 assert query.available_filters.has_key?('fixed_version_id')
37 assert query.available_filters.has_key?('fixed_version_id')
38 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
38 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
39 end
39 end
40
40
41 def test_project_filter_in_global_queries
41 def test_project_filter_in_global_queries
42 query = Query.new(:project => nil, :name => '_')
42 query = Query.new(:project => nil, :name => '_')
43 project_filter = query.available_filters["project_id"]
43 project_filter = query.available_filters["project_id"]
44 assert_not_nil project_filter
44 assert_not_nil project_filter
45 project_ids = project_filter[:values].map{|p| p[1]}
45 project_ids = project_filter[:values].map{|p| p[1]}
46 assert project_ids.include?("1") #public project
46 assert project_ids.include?("1") #public project
47 assert !project_ids.include?("2") #private project user cannot see
47 assert !project_ids.include?("2") #private project user cannot see
48 end
48 end
49
49
50 def find_issues_with_query(query)
50 def find_issues_with_query(query)
51 Issue.find :all,
51 Issue.find :all,
52 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
52 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
53 :conditions => query.statement
53 :conditions => query.statement
54 end
54 end
55
55
56 def assert_find_issues_with_query_is_successful(query)
56 def assert_find_issues_with_query_is_successful(query)
57 assert_nothing_raised do
57 assert_nothing_raised do
58 find_issues_with_query(query)
58 find_issues_with_query(query)
59 end
59 end
60 end
60 end
61
61
62 def assert_query_statement_includes(query, condition)
62 def assert_query_statement_includes(query, condition)
63 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
63 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
64 end
64 end
65
65
66 def assert_query_result(expected, query)
66 def assert_query_result(expected, query)
67 assert_nothing_raised do
67 assert_nothing_raised do
68 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
68 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
69 assert_equal expected.size, query.issue_count
69 assert_equal expected.size, query.issue_count
70 end
70 end
71 end
71 end
72
72
73 def test_query_should_allow_shared_versions_for_a_project_query
73 def test_query_should_allow_shared_versions_for_a_project_query
74 subproject_version = Version.find(4)
74 subproject_version = Version.find(4)
75 query = Query.new(:project => Project.find(1), :name => '_')
75 query = Query.new(:project => Project.find(1), :name => '_')
76 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
76 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
77
77
78 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
78 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
79 end
79 end
80
80
81 def test_query_with_multiple_custom_fields
81 def test_query_with_multiple_custom_fields
82 query = Query.find(1)
82 query = Query.find(1)
83 assert query.valid?
83 assert query.valid?
84 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
84 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
85 issues = find_issues_with_query(query)
85 issues = find_issues_with_query(query)
86 assert_equal 1, issues.length
86 assert_equal 1, issues.length
87 assert_equal Issue.find(3), issues.first
87 assert_equal Issue.find(3), issues.first
88 end
88 end
89
89
90 def test_operator_none
90 def test_operator_none
91 query = Query.new(:project => Project.find(1), :name => '_')
91 query = Query.new(:project => Project.find(1), :name => '_')
92 query.add_filter('fixed_version_id', '!*', [''])
92 query.add_filter('fixed_version_id', '!*', [''])
93 query.add_filter('cf_1', '!*', [''])
93 query.add_filter('cf_1', '!*', [''])
94 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
94 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
95 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
95 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
96 find_issues_with_query(query)
96 find_issues_with_query(query)
97 end
97 end
98
98
99 def test_operator_none_for_integer
99 def test_operator_none_for_integer
100 query = Query.new(:project => Project.find(1), :name => '_')
100 query = Query.new(:project => Project.find(1), :name => '_')
101 query.add_filter('estimated_hours', '!*', [''])
101 query.add_filter('estimated_hours', '!*', [''])
102 issues = find_issues_with_query(query)
102 issues = find_issues_with_query(query)
103 assert !issues.empty?
103 assert !issues.empty?
104 assert issues.all? {|i| !i.estimated_hours}
104 assert issues.all? {|i| !i.estimated_hours}
105 end
105 end
106
106
107 def test_operator_none_for_date
107 def test_operator_none_for_date
108 query = Query.new(:project => Project.find(1), :name => '_')
108 query = Query.new(:project => Project.find(1), :name => '_')
109 query.add_filter('start_date', '!*', [''])
109 query.add_filter('start_date', '!*', [''])
110 issues = find_issues_with_query(query)
110 issues = find_issues_with_query(query)
111 assert !issues.empty?
111 assert !issues.empty?
112 assert issues.all? {|i| i.start_date.nil?}
112 assert issues.all? {|i| i.start_date.nil?}
113 end
113 end
114
114
115 def test_operator_all
115 def test_operator_all
116 query = Query.new(:project => Project.find(1), :name => '_')
116 query = Query.new(:project => Project.find(1), :name => '_')
117 query.add_filter('fixed_version_id', '*', [''])
117 query.add_filter('fixed_version_id', '*', [''])
118 query.add_filter('cf_1', '*', [''])
118 query.add_filter('cf_1', '*', [''])
119 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
119 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
120 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
120 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
121 find_issues_with_query(query)
121 find_issues_with_query(query)
122 end
122 end
123
123
124 def test_operator_all_for_date
124 def test_operator_all_for_date
125 query = Query.new(:project => Project.find(1), :name => '_')
125 query = Query.new(:project => Project.find(1), :name => '_')
126 query.add_filter('start_date', '*', [''])
126 query.add_filter('start_date', '*', [''])
127 issues = find_issues_with_query(query)
127 issues = find_issues_with_query(query)
128 assert !issues.empty?
128 assert !issues.empty?
129 assert issues.all? {|i| i.start_date.present?}
129 assert issues.all? {|i| i.start_date.present?}
130 end
130 end
131
131
132 def test_numeric_filter_should_not_accept_non_numeric_values
132 def test_numeric_filter_should_not_accept_non_numeric_values
133 query = Query.new(:name => '_')
133 query = Query.new(:name => '_')
134 query.add_filter('estimated_hours', '=', ['a'])
134 query.add_filter('estimated_hours', '=', ['a'])
135
135
136 assert query.has_filter?('estimated_hours')
136 assert query.has_filter?('estimated_hours')
137 assert !query.valid?
137 assert !query.valid?
138 end
138 end
139
139
140 def test_operator_is_on_float
140 def test_operator_is_on_float
141 Issue.update_all("estimated_hours = 171.2", "id=2")
141 Issue.update_all("estimated_hours = 171.2", "id=2")
142
142
143 query = Query.new(:name => '_')
143 query = Query.new(:name => '_')
144 query.add_filter('estimated_hours', '=', ['171.20'])
144 query.add_filter('estimated_hours', '=', ['171.20'])
145 issues = find_issues_with_query(query)
145 issues = find_issues_with_query(query)
146 assert_equal 1, issues.size
146 assert_equal 1, issues.size
147 assert_equal 2, issues.first.id
147 assert_equal 2, issues.first.id
148 end
148 end
149
149
150 def test_operator_is_on_integer_custom_field
150 def test_operator_is_on_integer_custom_field
151 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
151 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
152 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
152 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
153 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
153 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
154 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
154 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
155
155
156 query = Query.new(:name => '_')
156 query = Query.new(:name => '_')
157 query.add_filter("cf_#{f.id}", '=', ['12'])
157 query.add_filter("cf_#{f.id}", '=', ['12'])
158 issues = find_issues_with_query(query)
158 issues = find_issues_with_query(query)
159 assert_equal 1, issues.size
159 assert_equal 1, issues.size
160 assert_equal 2, issues.first.id
160 assert_equal 2, issues.first.id
161 end
161 end
162
162
163 def test_operator_is_on_float_custom_field
163 def test_operator_is_on_float_custom_field
164 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
164 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
165 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
165 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
166 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
166 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
167 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
167 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
168
168
169 query = Query.new(:name => '_')
169 query = Query.new(:name => '_')
170 query.add_filter("cf_#{f.id}", '=', ['12.7'])
170 query.add_filter("cf_#{f.id}", '=', ['12.7'])
171 issues = find_issues_with_query(query)
171 issues = find_issues_with_query(query)
172 assert_equal 1, issues.size
172 assert_equal 1, issues.size
173 assert_equal 2, issues.first.id
173 assert_equal 2, issues.first.id
174 end
174 end
175
175
176 def test_operator_is_on_multi_list_custom_field
176 def test_operator_is_on_multi_list_custom_field
177 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
177 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
178 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
178 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
179 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
179 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
180 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
180 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
181 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
181 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
182
182
183 query = Query.new(:name => '_')
183 query = Query.new(:name => '_')
184 query.add_filter("cf_#{f.id}", '=', ['value1'])
184 query.add_filter("cf_#{f.id}", '=', ['value1'])
185 issues = find_issues_with_query(query)
185 issues = find_issues_with_query(query)
186 assert_equal [1, 3], issues.map(&:id).sort
186 assert_equal [1, 3], issues.map(&:id).sort
187
187
188 query = Query.new(:name => '_')
188 query = Query.new(:name => '_')
189 query.add_filter("cf_#{f.id}", '=', ['value2'])
189 query.add_filter("cf_#{f.id}", '=', ['value2'])
190 issues = find_issues_with_query(query)
190 issues = find_issues_with_query(query)
191 assert_equal [1], issues.map(&:id).sort
191 assert_equal [1], issues.map(&:id).sort
192 end
192 end
193
193
194 def test_operator_is_not_on_multi_list_custom_field
194 def test_operator_is_not_on_multi_list_custom_field
195 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
195 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
196 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
196 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
197 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
197 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
198 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
198 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
199 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
199 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
200
200
201 query = Query.new(:name => '_')
201 query = Query.new(:name => '_')
202 query.add_filter("cf_#{f.id}", '!', ['value1'])
202 query.add_filter("cf_#{f.id}", '!', ['value1'])
203 issues = find_issues_with_query(query)
203 issues = find_issues_with_query(query)
204 assert !issues.map(&:id).include?(1)
204 assert !issues.map(&:id).include?(1)
205 assert !issues.map(&:id).include?(3)
205 assert !issues.map(&:id).include?(3)
206
206
207 query = Query.new(:name => '_')
207 query = Query.new(:name => '_')
208 query.add_filter("cf_#{f.id}", '!', ['value2'])
208 query.add_filter("cf_#{f.id}", '!', ['value2'])
209 issues = find_issues_with_query(query)
209 issues = find_issues_with_query(query)
210 assert !issues.map(&:id).include?(1)
210 assert !issues.map(&:id).include?(1)
211 assert issues.map(&:id).include?(3)
211 assert issues.map(&:id).include?(3)
212 end
212 end
213
213
214 def test_operator_greater_than
214 def test_operator_greater_than
215 query = Query.new(:project => Project.find(1), :name => '_')
215 query = Query.new(:project => Project.find(1), :name => '_')
216 query.add_filter('done_ratio', '>=', ['40'])
216 query.add_filter('done_ratio', '>=', ['40'])
217 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
217 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
218 find_issues_with_query(query)
218 find_issues_with_query(query)
219 end
219 end
220
220
221 def test_operator_greater_than_a_float
221 def test_operator_greater_than_a_float
222 query = Query.new(:project => Project.find(1), :name => '_')
222 query = Query.new(:project => Project.find(1), :name => '_')
223 query.add_filter('estimated_hours', '>=', ['40.5'])
223 query.add_filter('estimated_hours', '>=', ['40.5'])
224 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
224 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
225 find_issues_with_query(query)
225 find_issues_with_query(query)
226 end
226 end
227
227
228 def test_operator_greater_than_on_int_custom_field
228 def test_operator_greater_than_on_int_custom_field
229 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
229 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
230 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
230 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
231 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
231 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
232 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
232 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
233
233
234 query = Query.new(:project => Project.find(1), :name => '_')
234 query = Query.new(:project => Project.find(1), :name => '_')
235 query.add_filter("cf_#{f.id}", '>=', ['8'])
235 query.add_filter("cf_#{f.id}", '>=', ['8'])
236 issues = find_issues_with_query(query)
236 issues = find_issues_with_query(query)
237 assert_equal 1, issues.size
237 assert_equal 1, issues.size
238 assert_equal 2, issues.first.id
238 assert_equal 2, issues.first.id
239 end
239 end
240
240
241 def test_operator_lesser_than
241 def test_operator_lesser_than
242 query = Query.new(:project => Project.find(1), :name => '_')
242 query = Query.new(:project => Project.find(1), :name => '_')
243 query.add_filter('done_ratio', '<=', ['30'])
243 query.add_filter('done_ratio', '<=', ['30'])
244 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
244 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
245 find_issues_with_query(query)
245 find_issues_with_query(query)
246 end
246 end
247
247
248 def test_operator_lesser_than_on_custom_field
248 def test_operator_lesser_than_on_custom_field
249 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
249 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
250 query = Query.new(:project => Project.find(1), :name => '_')
250 query = Query.new(:project => Project.find(1), :name => '_')
251 query.add_filter("cf_#{f.id}", '<=', ['30'])
251 query.add_filter("cf_#{f.id}", '<=', ['30'])
252 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
252 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
253 find_issues_with_query(query)
253 find_issues_with_query(query)
254 end
254 end
255
255
256 def test_operator_between
256 def test_operator_between
257 query = Query.new(:project => Project.find(1), :name => '_')
257 query = Query.new(:project => Project.find(1), :name => '_')
258 query.add_filter('done_ratio', '><', ['30', '40'])
258 query.add_filter('done_ratio', '><', ['30', '40'])
259 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
259 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
260 find_issues_with_query(query)
260 find_issues_with_query(query)
261 end
261 end
262
262
263 def test_operator_between_on_custom_field
263 def test_operator_between_on_custom_field
264 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
264 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
265 query = Query.new(:project => Project.find(1), :name => '_')
265 query = Query.new(:project => Project.find(1), :name => '_')
266 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
266 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
267 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
267 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
268 find_issues_with_query(query)
268 find_issues_with_query(query)
269 end
269 end
270
270
271 def test_date_filter_should_not_accept_non_date_values
271 def test_date_filter_should_not_accept_non_date_values
272 query = Query.new(:name => '_')
272 query = Query.new(:name => '_')
273 query.add_filter('created_on', '=', ['a'])
273 query.add_filter('created_on', '=', ['a'])
274
274
275 assert query.has_filter?('created_on')
275 assert query.has_filter?('created_on')
276 assert !query.valid?
276 assert !query.valid?
277 end
277 end
278
278
279 def test_date_filter_should_not_accept_invalid_date_values
279 def test_date_filter_should_not_accept_invalid_date_values
280 query = Query.new(:name => '_')
280 query = Query.new(:name => '_')
281 query.add_filter('created_on', '=', ['2011-01-34'])
281 query.add_filter('created_on', '=', ['2011-01-34'])
282
282
283 assert query.has_filter?('created_on')
283 assert query.has_filter?('created_on')
284 assert !query.valid?
284 assert !query.valid?
285 end
285 end
286
286
287 def test_relative_date_filter_should_not_accept_non_integer_values
287 def test_relative_date_filter_should_not_accept_non_integer_values
288 query = Query.new(:name => '_')
288 query = Query.new(:name => '_')
289 query.add_filter('created_on', '>t-', ['a'])
289 query.add_filter('created_on', '>t-', ['a'])
290
290
291 assert query.has_filter?('created_on')
291 assert query.has_filter?('created_on')
292 assert !query.valid?
292 assert !query.valid?
293 end
293 end
294
294
295 def test_operator_date_equals
295 def test_operator_date_equals
296 query = Query.new(:name => '_')
296 query = Query.new(:name => '_')
297 query.add_filter('due_date', '=', ['2011-07-10'])
297 query.add_filter('due_date', '=', ['2011-07-10'])
298 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
298 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
299 find_issues_with_query(query)
299 find_issues_with_query(query)
300 end
300 end
301
301
302 def test_operator_date_lesser_than
302 def test_operator_date_lesser_than
303 query = Query.new(:name => '_')
303 query = Query.new(:name => '_')
304 query.add_filter('due_date', '<=', ['2011-07-10'])
304 query.add_filter('due_date', '<=', ['2011-07-10'])
305 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
305 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
306 find_issues_with_query(query)
306 find_issues_with_query(query)
307 end
307 end
308
308
309 def test_operator_date_greater_than
309 def test_operator_date_greater_than
310 query = Query.new(:name => '_')
310 query = Query.new(:name => '_')
311 query.add_filter('due_date', '>=', ['2011-07-10'])
311 query.add_filter('due_date', '>=', ['2011-07-10'])
312 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
312 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
313 find_issues_with_query(query)
313 find_issues_with_query(query)
314 end
314 end
315
315
316 def test_operator_date_between
316 def test_operator_date_between
317 query = Query.new(:name => '_')
317 query = Query.new(:name => '_')
318 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
318 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
319 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
319 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
320 find_issues_with_query(query)
320 find_issues_with_query(query)
321 end
321 end
322
322
323 def test_operator_in_more_than
323 def test_operator_in_more_than
324 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
324 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
325 query = Query.new(:project => Project.find(1), :name => '_')
325 query = Query.new(:project => Project.find(1), :name => '_')
326 query.add_filter('due_date', '>t+', ['15'])
326 query.add_filter('due_date', '>t+', ['15'])
327 issues = find_issues_with_query(query)
327 issues = find_issues_with_query(query)
328 assert !issues.empty?
328 assert !issues.empty?
329 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
329 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
330 end
330 end
331
331
332 def test_operator_in_less_than
332 def test_operator_in_less_than
333 query = Query.new(:project => Project.find(1), :name => '_')
333 query = Query.new(:project => Project.find(1), :name => '_')
334 query.add_filter('due_date', '<t+', ['15'])
334 query.add_filter('due_date', '<t+', ['15'])
335 issues = find_issues_with_query(query)
335 issues = find_issues_with_query(query)
336 assert !issues.empty?
336 assert !issues.empty?
337 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
337 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
338 end
338 end
339
339
340 def test_operator_less_than_ago
340 def test_operator_less_than_ago
341 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
341 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
342 query = Query.new(:project => Project.find(1), :name => '_')
342 query = Query.new(:project => Project.find(1), :name => '_')
343 query.add_filter('due_date', '>t-', ['3'])
343 query.add_filter('due_date', '>t-', ['3'])
344 issues = find_issues_with_query(query)
344 issues = find_issues_with_query(query)
345 assert !issues.empty?
345 assert !issues.empty?
346 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
346 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
347 end
347 end
348
348
349 def test_operator_more_than_ago
349 def test_operator_more_than_ago
350 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
350 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
351 query = Query.new(:project => Project.find(1), :name => '_')
351 query = Query.new(:project => Project.find(1), :name => '_')
352 query.add_filter('due_date', '<t-', ['10'])
352 query.add_filter('due_date', '<t-', ['10'])
353 assert query.statement.include?("#{Issue.table_name}.due_date <=")
353 assert query.statement.include?("#{Issue.table_name}.due_date <=")
354 issues = find_issues_with_query(query)
354 issues = find_issues_with_query(query)
355 assert !issues.empty?
355 assert !issues.empty?
356 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
356 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
357 end
357 end
358
358
359 def test_operator_in
359 def test_operator_in
360 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
360 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
361 query = Query.new(:project => Project.find(1), :name => '_')
361 query = Query.new(:project => Project.find(1), :name => '_')
362 query.add_filter('due_date', 't+', ['2'])
362 query.add_filter('due_date', 't+', ['2'])
363 issues = find_issues_with_query(query)
363 issues = find_issues_with_query(query)
364 assert !issues.empty?
364 assert !issues.empty?
365 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
365 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
366 end
366 end
367
367
368 def test_operator_ago
368 def test_operator_ago
369 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
369 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
370 query = Query.new(:project => Project.find(1), :name => '_')
370 query = Query.new(:project => Project.find(1), :name => '_')
371 query.add_filter('due_date', 't-', ['3'])
371 query.add_filter('due_date', 't-', ['3'])
372 issues = find_issues_with_query(query)
372 issues = find_issues_with_query(query)
373 assert !issues.empty?
373 assert !issues.empty?
374 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
374 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
375 end
375 end
376
376
377 def test_operator_today
377 def test_operator_today
378 query = Query.new(:project => Project.find(1), :name => '_')
378 query = Query.new(:project => Project.find(1), :name => '_')
379 query.add_filter('due_date', 't', [''])
379 query.add_filter('due_date', 't', [''])
380 issues = find_issues_with_query(query)
380 issues = find_issues_with_query(query)
381 assert !issues.empty?
381 assert !issues.empty?
382 issues.each {|issue| assert_equal Date.today, issue.due_date}
382 issues.each {|issue| assert_equal Date.today, issue.due_date}
383 end
383 end
384
384
385 def test_operator_this_week_on_date
385 def test_operator_this_week_on_date
386 query = Query.new(:project => Project.find(1), :name => '_')
386 query = Query.new(:project => Project.find(1), :name => '_')
387 query.add_filter('due_date', 'w', [''])
387 query.add_filter('due_date', 'w', [''])
388 find_issues_with_query(query)
388 find_issues_with_query(query)
389 end
389 end
390
390
391 def test_operator_this_week_on_datetime
391 def test_operator_this_week_on_datetime
392 query = Query.new(:project => Project.find(1), :name => '_')
392 query = Query.new(:project => Project.find(1), :name => '_')
393 query.add_filter('created_on', 'w', [''])
393 query.add_filter('created_on', 'w', [''])
394 find_issues_with_query(query)
394 find_issues_with_query(query)
395 end
395 end
396
396
397 def test_operator_contains
397 def test_operator_contains
398 query = Query.new(:project => Project.find(1), :name => '_')
398 query = Query.new(:project => Project.find(1), :name => '_')
399 query.add_filter('subject', '~', ['uNable'])
399 query.add_filter('subject', '~', ['uNable'])
400 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
400 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
401 result = find_issues_with_query(query)
401 result = find_issues_with_query(query)
402 assert result.empty?
402 assert result.empty?
403 result.each {|issue| assert issue.subject.downcase.include?('unable') }
403 result.each {|issue| assert issue.subject.downcase.include?('unable') }
404 end
404 end
405
405
406 def test_range_for_this_week_with_week_starting_on_monday
406 def test_range_for_this_week_with_week_starting_on_monday
407 I18n.locale = :fr
407 I18n.locale = :fr
408 assert_equal '1', I18n.t(:general_first_day_of_week)
408 assert_equal '1', I18n.t(:general_first_day_of_week)
409
409
410 Date.stubs(:today).returns(Date.parse('2011-04-29'))
410 Date.stubs(:today).returns(Date.parse('2011-04-29'))
411
411
412 query = Query.new(:project => Project.find(1), :name => '_')
412 query = Query.new(:project => Project.find(1), :name => '_')
413 query.add_filter('due_date', 'w', [''])
413 query.add_filter('due_date', 'w', [''])
414 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
414 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
415 I18n.locale = :en
415 I18n.locale = :en
416 end
416 end
417
417
418 def test_range_for_this_week_with_week_starting_on_sunday
418 def test_range_for_this_week_with_week_starting_on_sunday
419 I18n.locale = :en
419 I18n.locale = :en
420 assert_equal '7', I18n.t(:general_first_day_of_week)
420 assert_equal '7', I18n.t(:general_first_day_of_week)
421
421
422 Date.stubs(:today).returns(Date.parse('2011-04-29'))
422 Date.stubs(:today).returns(Date.parse('2011-04-29'))
423
423
424 query = Query.new(:project => Project.find(1), :name => '_')
424 query = Query.new(:project => Project.find(1), :name => '_')
425 query.add_filter('due_date', 'w', [''])
425 query.add_filter('due_date', 'w', [''])
426 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
426 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
427 end
427 end
428
428
429 def test_operator_does_not_contains
429 def test_operator_does_not_contains
430 query = Query.new(:project => Project.find(1), :name => '_')
430 query = Query.new(:project => Project.find(1), :name => '_')
431 query.add_filter('subject', '!~', ['uNable'])
431 query.add_filter('subject', '!~', ['uNable'])
432 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
432 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
433 find_issues_with_query(query)
433 find_issues_with_query(query)
434 end
434 end
435
435
436 def test_filter_assigned_to_me
436 def test_filter_assigned_to_me
437 user = User.find(2)
437 user = User.find(2)
438 group = Group.find(10)
438 group = Group.find(10)
439 User.current = user
439 User.current = user
440 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
440 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
441 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
441 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
442 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
442 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
443 group.users << user
443 group.users << user
444
444
445 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
445 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
446 result = query.issues
446 result = query.issues
447 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
447 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
448
448
449 assert result.include?(i1)
449 assert result.include?(i1)
450 assert result.include?(i2)
450 assert result.include?(i2)
451 assert !result.include?(i3)
451 assert !result.include?(i3)
452 end
452 end
453
453
454 def test_user_custom_field_filtered_on_me
454 def test_user_custom_field_filtered_on_me
455 User.current = User.find(2)
455 User.current = User.find(2)
456 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
456 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
457 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
457 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
458 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
458 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
459
459
460 query = Query.new(:name => '_', :project => Project.find(1))
460 query = Query.new(:name => '_', :project => Project.find(1))
461 filter = query.available_filters["cf_#{cf.id}"]
461 filter = query.available_filters["cf_#{cf.id}"]
462 assert_not_nil filter
462 assert_not_nil filter
463 assert_include 'me', filter[:values].map{|v| v[1]}
463 assert_include 'me', filter[:values].map{|v| v[1]}
464
464
465 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
465 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
466 result = query.issues
466 result = query.issues
467 assert_equal 1, result.size
467 assert_equal 1, result.size
468 assert_equal issue1, result.first
468 assert_equal issue1, result.first
469 end
469 end
470
470
471 def test_filter_my_projects
471 def test_filter_my_projects
472 User.current = User.find(2)
472 User.current = User.find(2)
473 query = Query.new(:name => '_')
473 query = Query.new(:name => '_')
474 filter = query.available_filters['project_id']
474 filter = query.available_filters['project_id']
475 assert_not_nil filter
475 assert_not_nil filter
476 assert_include 'mine', filter[:values].map{|v| v[1]}
476 assert_include 'mine', filter[:values].map{|v| v[1]}
477
477
478 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
478 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
479 result = query.issues
479 result = query.issues
480 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
480 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
481 end
481 end
482
482
483 def test_filter_watched_issues
483 def test_filter_watched_issues
484 User.current = User.find(1)
484 User.current = User.find(1)
485 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
485 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
486 result = find_issues_with_query(query)
486 result = find_issues_with_query(query)
487 assert_not_nil result
487 assert_not_nil result
488 assert !result.empty?
488 assert !result.empty?
489 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
489 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
490 User.current = nil
490 User.current = nil
491 end
491 end
492
492
493 def test_filter_unwatched_issues
493 def test_filter_unwatched_issues
494 User.current = User.find(1)
494 User.current = User.find(1)
495 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
495 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
496 result = find_issues_with_query(query)
496 result = find_issues_with_query(query)
497 assert_not_nil result
497 assert_not_nil result
498 assert !result.empty?
498 assert !result.empty?
499 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
499 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
500 User.current = nil
500 User.current = nil
501 end
501 end
502
502
503 def test_statement_should_be_nil_with_no_filters
503 def test_statement_should_be_nil_with_no_filters
504 q = Query.new(:name => '_')
504 q = Query.new(:name => '_')
505 q.filters = {}
505 q.filters = {}
506
506
507 assert q.valid?
507 assert q.valid?
508 assert_nil q.statement
508 assert_nil q.statement
509 end
509 end
510
510
511 def test_default_columns
511 def test_default_columns
512 q = Query.new
512 q = Query.new
513 assert !q.columns.empty?
513 assert !q.columns.empty?
514 end
514 end
515
515
516 def test_set_column_names
516 def test_set_column_names
517 q = Query.new
517 q = Query.new
518 q.column_names = ['tracker', :subject, '', 'unknonw_column']
518 q.column_names = ['tracker', :subject, '', 'unknonw_column']
519 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
519 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
520 c = q.columns.first
520 c = q.columns.first
521 assert q.has_column?(c)
521 assert q.has_column?(c)
522 end
522 end
523
523
524 def test_query_should_preload_spent_hours
524 def test_query_should_preload_spent_hours
525 q = Query.new(:name => '_', :column_names => [:subject, :spent_hours])
525 q = Query.new(:name => '_', :column_names => [:subject, :spent_hours])
526 assert q.has_column?(:spent_hours)
526 assert q.has_column?(:spent_hours)
527 issues = q.issues
527 issues = q.issues
528 assert_not_nil issues.first.instance_variable_get("@spent_hours")
528 assert_not_nil issues.first.instance_variable_get("@spent_hours")
529 end
529 end
530
530
531 def test_groupable_columns_should_include_custom_fields
531 def test_groupable_columns_should_include_custom_fields
532 q = Query.new
532 q = Query.new
533 column = q.groupable_columns.detect {|c| c.name == :cf_1}
533 column = q.groupable_columns.detect {|c| c.name == :cf_1}
534 assert_not_nil column
534 assert_not_nil column
535 assert_kind_of QueryCustomFieldColumn, column
535 assert_kind_of QueryCustomFieldColumn, column
536 end
536 end
537
537
538 def test_groupable_columns_should_not_include_multi_custom_fields
538 def test_groupable_columns_should_not_include_multi_custom_fields
539 field = CustomField.find(1)
539 field = CustomField.find(1)
540 field.update_attribute :multiple, true
540 field.update_attribute :multiple, true
541
541
542 q = Query.new
542 q = Query.new
543 column = q.groupable_columns.detect {|c| c.name == :cf_1}
543 column = q.groupable_columns.detect {|c| c.name == :cf_1}
544 assert_nil column
544 assert_nil column
545 end
545 end
546
546
547 def test_grouped_with_valid_column
547 def test_grouped_with_valid_column
548 q = Query.new(:group_by => 'status')
548 q = Query.new(:group_by => 'status')
549 assert q.grouped?
549 assert q.grouped?
550 assert_not_nil q.group_by_column
550 assert_not_nil q.group_by_column
551 assert_equal :status, q.group_by_column.name
551 assert_equal :status, q.group_by_column.name
552 assert_not_nil q.group_by_statement
552 assert_not_nil q.group_by_statement
553 assert_equal 'status', q.group_by_statement
553 assert_equal 'status', q.group_by_statement
554 end
554 end
555
555
556 def test_grouped_with_invalid_column
556 def test_grouped_with_invalid_column
557 q = Query.new(:group_by => 'foo')
557 q = Query.new(:group_by => 'foo')
558 assert !q.grouped?
558 assert !q.grouped?
559 assert_nil q.group_by_column
559 assert_nil q.group_by_column
560 assert_nil q.group_by_statement
560 assert_nil q.group_by_statement
561 end
561 end
562
562
563 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
563 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
564 with_settings :user_format => 'lastname_coma_firstname' do
564 with_settings :user_format => 'lastname_coma_firstname' do
565 q = Query.new
565 q = Query.new
566 assert q.sortable_columns.has_key?('assigned_to')
566 assert q.sortable_columns.has_key?('assigned_to')
567 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
567 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
568 end
568 end
569 end
569 end
570
570
571 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
571 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
572 with_settings :user_format => 'lastname_coma_firstname' do
572 with_settings :user_format => 'lastname_coma_firstname' do
573 q = Query.new
573 q = Query.new
574 assert q.sortable_columns.has_key?('author')
574 assert q.sortable_columns.has_key?('author')
575 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
575 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
576 end
576 end
577 end
577 end
578
578
579 def test_sortable_columns_should_include_custom_field
579 def test_sortable_columns_should_include_custom_field
580 q = Query.new
580 q = Query.new
581 assert q.sortable_columns['cf_1']
581 assert q.sortable_columns['cf_1']
582 end
582 end
583
583
584 def test_sortable_columns_should_not_include_multi_custom_field
584 def test_sortable_columns_should_not_include_multi_custom_field
585 field = CustomField.find(1)
585 field = CustomField.find(1)
586 field.update_attribute :multiple, true
586 field.update_attribute :multiple, true
587
587
588 q = Query.new
588 q = Query.new
589 assert !q.sortable_columns['cf_1']
589 assert !q.sortable_columns['cf_1']
590 end
590 end
591
591
592 def test_default_sort
592 def test_default_sort
593 q = Query.new
593 q = Query.new
594 assert_equal [], q.sort_criteria
594 assert_equal [], q.sort_criteria
595 end
595 end
596
596
597 def test_set_sort_criteria_with_hash
597 def test_set_sort_criteria_with_hash
598 q = Query.new
598 q = Query.new
599 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
599 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
600 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
600 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
601 end
601 end
602
602
603 def test_set_sort_criteria_with_array
603 def test_set_sort_criteria_with_array
604 q = Query.new
604 q = Query.new
605 q.sort_criteria = [['priority', 'desc'], 'tracker']
605 q.sort_criteria = [['priority', 'desc'], 'tracker']
606 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
606 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
607 end
607 end
608
608
609 def test_create_query_with_sort
609 def test_create_query_with_sort
610 q = Query.new(:name => 'Sorted')
610 q = Query.new(:name => 'Sorted')
611 q.sort_criteria = [['priority', 'desc'], 'tracker']
611 q.sort_criteria = [['priority', 'desc'], 'tracker']
612 assert q.save
612 assert q.save
613 q.reload
613 q.reload
614 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
614 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
615 end
615 end
616
616
617 def test_sort_by_string_custom_field_asc
617 def test_sort_by_string_custom_field_asc
618 q = Query.new
618 q = Query.new
619 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
619 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
620 assert c
620 assert c
621 assert c.sortable
621 assert c.sortable
622 issues = Issue.find :all,
622 issues = Issue.find :all,
623 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
623 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
624 :conditions => q.statement,
624 :conditions => q.statement,
625 :order => "#{c.sortable} ASC"
625 :order => "#{c.sortable} ASC"
626 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
626 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
627 assert !values.empty?
627 assert !values.empty?
628 assert_equal values.sort, values
628 assert_equal values.sort, values
629 end
629 end
630
630
631 def test_sort_by_string_custom_field_desc
631 def test_sort_by_string_custom_field_desc
632 q = Query.new
632 q = Query.new
633 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
633 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
634 assert c
634 assert c
635 assert c.sortable
635 assert c.sortable
636 issues = Issue.find :all,
636 issues = Issue.find :all,
637 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
637 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
638 :conditions => q.statement,
638 :conditions => q.statement,
639 :order => "#{c.sortable} DESC"
639 :order => "#{c.sortable} DESC"
640 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
640 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
641 assert !values.empty?
641 assert !values.empty?
642 assert_equal values.sort.reverse, values
642 assert_equal values.sort.reverse, values
643 end
643 end
644
644
645 def test_sort_by_float_custom_field_asc
645 def test_sort_by_float_custom_field_asc
646 q = Query.new
646 q = Query.new
647 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
647 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
648 assert c
648 assert c
649 assert c.sortable
649 assert c.sortable
650 issues = Issue.find :all,
650 issues = Issue.find :all,
651 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
651 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
652 :conditions => q.statement,
652 :conditions => q.statement,
653 :order => "#{c.sortable} ASC"
653 :order => "#{c.sortable} ASC"
654 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
654 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
655 assert !values.empty?
655 assert !values.empty?
656 assert_equal values.sort, values
656 assert_equal values.sort, values
657 end
657 end
658
658
659 def test_invalid_query_should_raise_query_statement_invalid_error
659 def test_invalid_query_should_raise_query_statement_invalid_error
660 q = Query.new
660 q = Query.new
661 assert_raise Query::StatementInvalid do
661 assert_raise Query::StatementInvalid do
662 q.issues(:conditions => "foo = 1")
662 q.issues(:conditions => "foo = 1")
663 end
663 end
664 end
664 end
665
665
666 def test_issue_count
666 def test_issue_count
667 q = Query.new(:name => '_')
667 q = Query.new(:name => '_')
668 issue_count = q.issue_count
668 issue_count = q.issue_count
669 assert_equal q.issues.size, issue_count
669 assert_equal q.issues.size, issue_count
670 end
670 end
671
671
672 def test_issue_count_with_archived_issues
672 def test_issue_count_with_archived_issues
673 p = Project.generate!( :status => Project::STATUS_ARCHIVED )
673 p = Project.generate!( :status => Project::STATUS_ARCHIVED )
674 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
674 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
675 assert !i.visible?
675 assert !i.visible?
676
676
677 test_issue_count
677 test_issue_count
678 end
678 end
679
679
680 def test_issue_count_by_association_group
680 def test_issue_count_by_association_group
681 q = Query.new(:name => '_', :group_by => 'assigned_to')
681 q = Query.new(:name => '_', :group_by => 'assigned_to')
682 count_by_group = q.issue_count_by_group
682 count_by_group = q.issue_count_by_group
683 assert_kind_of Hash, count_by_group
683 assert_kind_of Hash, count_by_group
684 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
684 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
685 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
685 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
686 assert count_by_group.has_key?(User.find(3))
686 assert count_by_group.has_key?(User.find(3))
687 end
687 end
688
688
689 def test_issue_count_by_list_custom_field_group
689 def test_issue_count_by_list_custom_field_group
690 q = Query.new(:name => '_', :group_by => 'cf_1')
690 q = Query.new(:name => '_', :group_by => 'cf_1')
691 count_by_group = q.issue_count_by_group
691 count_by_group = q.issue_count_by_group
692 assert_kind_of Hash, count_by_group
692 assert_kind_of Hash, count_by_group
693 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
693 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
694 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
694 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
695 assert count_by_group.has_key?('MySQL')
695 assert count_by_group.has_key?('MySQL')
696 end
696 end
697
697
698 def test_issue_count_by_date_custom_field_group
698 def test_issue_count_by_date_custom_field_group
699 q = Query.new(:name => '_', :group_by => 'cf_8')
699 q = Query.new(:name => '_', :group_by => 'cf_8')
700 count_by_group = q.issue_count_by_group
700 count_by_group = q.issue_count_by_group
701 assert_kind_of Hash, count_by_group
701 assert_kind_of Hash, count_by_group
702 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
702 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
703 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
703 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
704 end
704 end
705
705
706 def test_issue_ids
706 def test_issue_ids
707 q = Query.new(:name => '_')
707 q = Query.new(:name => '_')
708 order = "issues.subject, issues.id"
708 order = "issues.subject, issues.id"
709 issues = q.issues(:order => order)
709 issues = q.issues(:order => order)
710 assert_equal issues.map(&:id), q.issue_ids(:order => order)
710 assert_equal issues.map(&:id), q.issue_ids(:order => order)
711 end
711 end
712
712
713 def test_label_for
713 def test_label_for
714 q = Query.new
714 q = Query.new
715 assert_equal 'Assignee', q.label_for('assigned_to_id')
715 assert_equal 'Assignee', q.label_for('assigned_to_id')
716 end
716 end
717
717
718 def test_editable_by
718 def test_editable_by
719 admin = User.find(1)
719 admin = User.find(1)
720 manager = User.find(2)
720 manager = User.find(2)
721 developer = User.find(3)
721 developer = User.find(3)
722
722
723 # Public query on project 1
723 # Public query on project 1
724 q = Query.find(1)
724 q = Query.find(1)
725 assert q.editable_by?(admin)
725 assert q.editable_by?(admin)
726 assert q.editable_by?(manager)
726 assert q.editable_by?(manager)
727 assert !q.editable_by?(developer)
727 assert !q.editable_by?(developer)
728
728
729 # Private query on project 1
729 # Private query on project 1
730 q = Query.find(2)
730 q = Query.find(2)
731 assert q.editable_by?(admin)
731 assert q.editable_by?(admin)
732 assert !q.editable_by?(manager)
732 assert !q.editable_by?(manager)
733 assert q.editable_by?(developer)
733 assert q.editable_by?(developer)
734
734
735 # Private query for all projects
735 # Private query for all projects
736 q = Query.find(3)
736 q = Query.find(3)
737 assert q.editable_by?(admin)
737 assert q.editable_by?(admin)
738 assert !q.editable_by?(manager)
738 assert !q.editable_by?(manager)
739 assert q.editable_by?(developer)
739 assert q.editable_by?(developer)
740
740
741 # Public query for all projects
741 # Public query for all projects
742 q = Query.find(4)
742 q = Query.find(4)
743 assert q.editable_by?(admin)
743 assert q.editable_by?(admin)
744 assert !q.editable_by?(manager)
744 assert !q.editable_by?(manager)
745 assert !q.editable_by?(developer)
745 assert !q.editable_by?(developer)
746 end
746 end
747
747
748 def test_visible_scope
748 def test_visible_scope
749 query_ids = Query.visible(User.anonymous).map(&:id)
749 query_ids = Query.visible(User.anonymous).map(&:id)
750
750
751 assert query_ids.include?(1), 'public query on public project was not visible'
751 assert query_ids.include?(1), 'public query on public project was not visible'
752 assert query_ids.include?(4), 'public query for all projects was not visible'
752 assert query_ids.include?(4), 'public query for all projects was not visible'
753 assert !query_ids.include?(2), 'private query on public project was visible'
753 assert !query_ids.include?(2), 'private query on public project was visible'
754 assert !query_ids.include?(3), 'private query for all projects was visible'
754 assert !query_ids.include?(3), 'private query for all projects was visible'
755 assert !query_ids.include?(7), 'public query on private project was visible'
755 assert !query_ids.include?(7), 'public query on private project was visible'
756 end
756 end
757
757
758 context "#available_filters" do
758 context "#available_filters" do
759 setup do
759 setup do
760 @query = Query.new(:name => "_")
760 @query = Query.new(:name => "_")
761 end
761 end
762
762
763 should "include users of visible projects in cross-project view" do
763 should "include users of visible projects in cross-project view" do
764 users = @query.available_filters["assigned_to_id"]
764 users = @query.available_filters["assigned_to_id"]
765 assert_not_nil users
765 assert_not_nil users
766 assert users[:values].map{|u|u[1]}.include?("3")
766 assert users[:values].map{|u|u[1]}.include?("3")
767 end
767 end
768
768
769 should "include users of subprojects" do
770 user1 = User.generate_with_protected!
771 user2 = User.generate_with_protected!
772 project = Project.find(1)
773 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
774 @query.project = project
775
776 users = @query.available_filters["assigned_to_id"]
777 assert_not_nil users
778 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
779 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
780 end
781
769 should "include visible projects in cross-project view" do
782 should "include visible projects in cross-project view" do
770 projects = @query.available_filters["project_id"]
783 projects = @query.available_filters["project_id"]
771 assert_not_nil projects
784 assert_not_nil projects
772 assert projects[:values].map{|u|u[1]}.include?("1")
785 assert projects[:values].map{|u|u[1]}.include?("1")
773 end
786 end
774
787
775 context "'member_of_group' filter" do
788 context "'member_of_group' filter" do
776 should "be present" do
789 should "be present" do
777 assert @query.available_filters.keys.include?("member_of_group")
790 assert @query.available_filters.keys.include?("member_of_group")
778 end
791 end
779
792
780 should "be an optional list" do
793 should "be an optional list" do
781 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
794 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
782 end
795 end
783
796
784 should "have a list of the groups as values" do
797 should "have a list of the groups as values" do
785 Group.destroy_all # No fixtures
798 Group.destroy_all # No fixtures
786 group1 = Group.generate!.reload
799 group1 = Group.generate!.reload
787 group2 = Group.generate!.reload
800 group2 = Group.generate!.reload
788
801
789 expected_group_list = [
802 expected_group_list = [
790 [group1.name, group1.id.to_s],
803 [group1.name, group1.id.to_s],
791 [group2.name, group2.id.to_s]
804 [group2.name, group2.id.to_s]
792 ]
805 ]
793 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
806 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
794 end
807 end
795
808
796 end
809 end
797
810
798 context "'assigned_to_role' filter" do
811 context "'assigned_to_role' filter" do
799 should "be present" do
812 should "be present" do
800 assert @query.available_filters.keys.include?("assigned_to_role")
813 assert @query.available_filters.keys.include?("assigned_to_role")
801 end
814 end
802
815
803 should "be an optional list" do
816 should "be an optional list" do
804 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
817 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
805 end
818 end
806
819
807 should "have a list of the Roles as values" do
820 should "have a list of the Roles as values" do
808 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
821 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
809 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
822 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
810 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
823 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
811 end
824 end
812
825
813 should "not include the built in Roles as values" do
826 should "not include the built in Roles as values" do
814 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
827 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
815 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
828 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
816 end
829 end
817
830
818 end
831 end
819
832
820 end
833 end
821
834
822 context "#statement" do
835 context "#statement" do
823 context "with 'member_of_group' filter" do
836 context "with 'member_of_group' filter" do
824 setup do
837 setup do
825 Group.destroy_all # No fixtures
838 Group.destroy_all # No fixtures
826 @user_in_group = User.generate!
839 @user_in_group = User.generate!
827 @second_user_in_group = User.generate!
840 @second_user_in_group = User.generate!
828 @user_in_group2 = User.generate!
841 @user_in_group2 = User.generate!
829 @user_not_in_group = User.generate!
842 @user_not_in_group = User.generate!
830
843
831 @group = Group.generate!.reload
844 @group = Group.generate!.reload
832 @group.users << @user_in_group
845 @group.users << @user_in_group
833 @group.users << @second_user_in_group
846 @group.users << @second_user_in_group
834
847
835 @group2 = Group.generate!.reload
848 @group2 = Group.generate!.reload
836 @group2.users << @user_in_group2
849 @group2.users << @user_in_group2
837
850
838 end
851 end
839
852
840 should "search assigned to for users in the group" do
853 should "search assigned to for users in the group" do
841 @query = Query.new(:name => '_')
854 @query = Query.new(:name => '_')
842 @query.add_filter('member_of_group', '=', [@group.id.to_s])
855 @query.add_filter('member_of_group', '=', [@group.id.to_s])
843
856
844 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
857 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
845 assert_find_issues_with_query_is_successful @query
858 assert_find_issues_with_query_is_successful @query
846 end
859 end
847
860
848 should "search not assigned to any group member (none)" do
861 should "search not assigned to any group member (none)" do
849 @query = Query.new(:name => '_')
862 @query = Query.new(:name => '_')
850 @query.add_filter('member_of_group', '!*', [''])
863 @query.add_filter('member_of_group', '!*', [''])
851
864
852 # Users not in a group
865 # Users not in a group
853 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}')"
866 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}')"
854 assert_find_issues_with_query_is_successful @query
867 assert_find_issues_with_query_is_successful @query
855 end
868 end
856
869
857 should "search assigned to any group member (all)" do
870 should "search assigned to any group member (all)" do
858 @query = Query.new(:name => '_')
871 @query = Query.new(:name => '_')
859 @query.add_filter('member_of_group', '*', [''])
872 @query.add_filter('member_of_group', '*', [''])
860
873
861 # Only users in a group
874 # Only users in a group
862 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}')"
875 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}')"
863 assert_find_issues_with_query_is_successful @query
876 assert_find_issues_with_query_is_successful @query
864 end
877 end
865
878
866 should "return an empty set with = empty group" do
879 should "return an empty set with = empty group" do
867 @empty_group = Group.generate!
880 @empty_group = Group.generate!
868 @query = Query.new(:name => '_')
881 @query = Query.new(:name => '_')
869 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
882 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
870
883
871 assert_equal [], find_issues_with_query(@query)
884 assert_equal [], find_issues_with_query(@query)
872 end
885 end
873
886
874 should "return issues with ! empty group" do
887 should "return issues with ! empty group" do
875 @empty_group = Group.generate!
888 @empty_group = Group.generate!
876 @query = Query.new(:name => '_')
889 @query = Query.new(:name => '_')
877 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
890 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
878
891
879 assert_find_issues_with_query_is_successful @query
892 assert_find_issues_with_query_is_successful @query
880 end
893 end
881 end
894 end
882
895
883 context "with 'assigned_to_role' filter" do
896 context "with 'assigned_to_role' filter" do
884 setup do
897 setup do
885 @manager_role = Role.find_by_name('Manager')
898 @manager_role = Role.find_by_name('Manager')
886 @developer_role = Role.find_by_name('Developer')
899 @developer_role = Role.find_by_name('Developer')
887
900
888 @project = Project.generate!
901 @project = Project.generate!
889 @manager = User.generate!
902 @manager = User.generate!
890 @developer = User.generate!
903 @developer = User.generate!
891 @boss = User.generate!
904 @boss = User.generate!
892 @guest = User.generate!
905 @guest = User.generate!
893 User.add_to_project(@manager, @project, @manager_role)
906 User.add_to_project(@manager, @project, @manager_role)
894 User.add_to_project(@developer, @project, @developer_role)
907 User.add_to_project(@developer, @project, @developer_role)
895 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
908 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
896
909
897 @issue1 = Issue.generate_for_project!(@project, :assigned_to_id => @manager.id)
910 @issue1 = Issue.generate_for_project!(@project, :assigned_to_id => @manager.id)
898 @issue2 = Issue.generate_for_project!(@project, :assigned_to_id => @developer.id)
911 @issue2 = Issue.generate_for_project!(@project, :assigned_to_id => @developer.id)
899 @issue3 = Issue.generate_for_project!(@project, :assigned_to_id => @boss.id)
912 @issue3 = Issue.generate_for_project!(@project, :assigned_to_id => @boss.id)
900 @issue4 = Issue.generate_for_project!(@project, :assigned_to_id => @guest.id)
913 @issue4 = Issue.generate_for_project!(@project, :assigned_to_id => @guest.id)
901 @issue5 = Issue.generate_for_project!(@project)
914 @issue5 = Issue.generate_for_project!(@project)
902 end
915 end
903
916
904 should "search assigned to for users with the Role" do
917 should "search assigned to for users with the Role" do
905 @query = Query.new(:name => '_', :project => @project)
918 @query = Query.new(:name => '_', :project => @project)
906 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
919 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
907
920
908 assert_query_result [@issue1, @issue3], @query
921 assert_query_result [@issue1, @issue3], @query
909 end
922 end
910
923
911 should "search assigned to for users with the Role on the issue project" do
924 should "search assigned to for users with the Role on the issue project" do
912 other_project = Project.generate!
925 other_project = Project.generate!
913 User.add_to_project(@developer, other_project, @manager_role)
926 User.add_to_project(@developer, other_project, @manager_role)
914
927
915 @query = Query.new(:name => '_', :project => @project)
928 @query = Query.new(:name => '_', :project => @project)
916 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
929 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
917
930
918 assert_query_result [@issue1, @issue3], @query
931 assert_query_result [@issue1, @issue3], @query
919 end
932 end
920
933
921 should "return an empty set with empty role" do
934 should "return an empty set with empty role" do
922 @empty_role = Role.generate!
935 @empty_role = Role.generate!
923 @query = Query.new(:name => '_', :project => @project)
936 @query = Query.new(:name => '_', :project => @project)
924 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
937 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
925
938
926 assert_query_result [], @query
939 assert_query_result [], @query
927 end
940 end
928
941
929 should "search assigned to for users without the Role" do
942 should "search assigned to for users without the Role" do
930 @query = Query.new(:name => '_', :project => @project)
943 @query = Query.new(:name => '_', :project => @project)
931 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
944 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
932
945
933 assert_query_result [@issue2, @issue4, @issue5], @query
946 assert_query_result [@issue2, @issue4, @issue5], @query
934 end
947 end
935
948
936 should "search assigned to for users not assigned to any Role (none)" do
949 should "search assigned to for users not assigned to any Role (none)" do
937 @query = Query.new(:name => '_', :project => @project)
950 @query = Query.new(:name => '_', :project => @project)
938 @query.add_filter('assigned_to_role', '!*', [''])
951 @query.add_filter('assigned_to_role', '!*', [''])
939
952
940 assert_query_result [@issue4, @issue5], @query
953 assert_query_result [@issue4, @issue5], @query
941 end
954 end
942
955
943 should "search assigned to for users assigned to any Role (all)" do
956 should "search assigned to for users assigned to any Role (all)" do
944 @query = Query.new(:name => '_', :project => @project)
957 @query = Query.new(:name => '_', :project => @project)
945 @query.add_filter('assigned_to_role', '*', [''])
958 @query.add_filter('assigned_to_role', '*', [''])
946
959
947 assert_query_result [@issue1, @issue2, @issue3], @query
960 assert_query_result [@issue1, @issue2, @issue3], @query
948 end
961 end
949
962
950 should "return issues with ! empty role" do
963 should "return issues with ! empty role" do
951 @empty_role = Role.generate!
964 @empty_role = Role.generate!
952 @query = Query.new(:name => '_', :project => @project)
965 @query = Query.new(:name => '_', :project => @project)
953 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
966 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
954
967
955 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
968 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
956 end
969 end
957 end
970 end
958 end
971 end
959
972
960 end
973 end
General Comments 0
You need to be logged in to leave comments. Login now