##// END OF EJS Templates
Adds a version filter on time entries (#13558)....
Jean-Philippe Lang -
r15264:539166597f99
parent child
Show More
@@ -1,1111 +1,1111
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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, :totalable, :default_order
19 attr_accessor :name, :sortable, :groupable, :totalable, :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.totalable = options[:totalable] || false
29 self.totalable = options[:totalable] || false
30 self.default_order = options[:default_order]
30 self.default_order = options[:default_order]
31 @inline = options.key?(:inline) ? options[:inline] : true
31 @inline = options.key?(:inline) ? options[:inline] : true
32 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @caption_key = options[:caption] || "field_#{name}".to_sym
33 @frozen = options[:frozen]
33 @frozen = options[:frozen]
34 end
34 end
35
35
36 def caption
36 def caption
37 case @caption_key
37 case @caption_key
38 when Symbol
38 when Symbol
39 l(@caption_key)
39 l(@caption_key)
40 when Proc
40 when Proc
41 @caption_key.call
41 @caption_key.call
42 else
42 else
43 @caption_key
43 @caption_key
44 end
44 end
45 end
45 end
46
46
47 # Returns true if the column is sortable, otherwise false
47 # Returns true if the column is sortable, otherwise false
48 def sortable?
48 def sortable?
49 !@sortable.nil?
49 !@sortable.nil?
50 end
50 end
51
51
52 def sortable
52 def sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
54 end
54 end
55
55
56 def inline?
56 def inline?
57 @inline
57 @inline
58 end
58 end
59
59
60 def frozen?
60 def frozen?
61 @frozen
61 @frozen
62 end
62 end
63
63
64 def value(object)
64 def value(object)
65 object.send name
65 object.send name
66 end
66 end
67
67
68 def value_object(object)
68 def value_object(object)
69 object.send name
69 object.send name
70 end
70 end
71
71
72 def css_classes
72 def css_classes
73 name
73 name
74 end
74 end
75 end
75 end
76
76
77 class QueryCustomFieldColumn < QueryColumn
77 class QueryCustomFieldColumn < QueryColumn
78
78
79 def initialize(custom_field)
79 def initialize(custom_field)
80 self.name = "cf_#{custom_field.id}".to_sym
80 self.name = "cf_#{custom_field.id}".to_sym
81 self.sortable = custom_field.order_statement || false
81 self.sortable = custom_field.order_statement || false
82 self.groupable = custom_field.group_statement || false
82 self.groupable = custom_field.group_statement || false
83 self.totalable = custom_field.totalable?
83 self.totalable = custom_field.totalable?
84 @inline = true
84 @inline = true
85 @cf = custom_field
85 @cf = custom_field
86 end
86 end
87
87
88 def caption
88 def caption
89 @cf.name
89 @cf.name
90 end
90 end
91
91
92 def custom_field
92 def custom_field
93 @cf
93 @cf
94 end
94 end
95
95
96 def value_object(object)
96 def value_object(object)
97 if custom_field.visible_by?(object.project, User.current)
97 if custom_field.visible_by?(object.project, User.current)
98 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
98 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
99 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
99 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
100 else
100 else
101 nil
101 nil
102 end
102 end
103 end
103 end
104
104
105 def value(object)
105 def value(object)
106 raw = value_object(object)
106 raw = value_object(object)
107 if raw.is_a?(Array)
107 if raw.is_a?(Array)
108 raw.map {|r| @cf.cast_value(r.value)}
108 raw.map {|r| @cf.cast_value(r.value)}
109 elsif raw
109 elsif raw
110 @cf.cast_value(raw.value)
110 @cf.cast_value(raw.value)
111 else
111 else
112 nil
112 nil
113 end
113 end
114 end
114 end
115
115
116 def css_classes
116 def css_classes
117 @css_classes ||= "#{name} #{@cf.field_format}"
117 @css_classes ||= "#{name} #{@cf.field_format}"
118 end
118 end
119 end
119 end
120
120
121 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
121 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
122
122
123 def initialize(association, custom_field)
123 def initialize(association, custom_field)
124 super(custom_field)
124 super(custom_field)
125 self.name = "#{association}.cf_#{custom_field.id}".to_sym
125 self.name = "#{association}.cf_#{custom_field.id}".to_sym
126 # TODO: support sorting/grouping by association custom field
126 # TODO: support sorting/grouping by association custom field
127 self.sortable = false
127 self.sortable = false
128 self.groupable = false
128 self.groupable = false
129 @association = association
129 @association = association
130 end
130 end
131
131
132 def value_object(object)
132 def value_object(object)
133 if assoc = object.send(@association)
133 if assoc = object.send(@association)
134 super(assoc)
134 super(assoc)
135 end
135 end
136 end
136 end
137
137
138 def css_classes
138 def css_classes
139 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
139 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
140 end
140 end
141 end
141 end
142
142
143 class Query < ActiveRecord::Base
143 class Query < ActiveRecord::Base
144 class StatementInvalid < ::ActiveRecord::StatementInvalid
144 class StatementInvalid < ::ActiveRecord::StatementInvalid
145 end
145 end
146
146
147 include Redmine::SubclassFactory
147 include Redmine::SubclassFactory
148
148
149 VISIBILITY_PRIVATE = 0
149 VISIBILITY_PRIVATE = 0
150 VISIBILITY_ROLES = 1
150 VISIBILITY_ROLES = 1
151 VISIBILITY_PUBLIC = 2
151 VISIBILITY_PUBLIC = 2
152
152
153 belongs_to :project
153 belongs_to :project
154 belongs_to :user
154 belongs_to :user
155 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
155 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
156 serialize :filters
156 serialize :filters
157 serialize :column_names
157 serialize :column_names
158 serialize :sort_criteria, Array
158 serialize :sort_criteria, Array
159 serialize :options, Hash
159 serialize :options, Hash
160
160
161 attr_protected :project_id, :user_id
161 attr_protected :project_id, :user_id
162
162
163 validates_presence_of :name
163 validates_presence_of :name
164 validates_length_of :name, :maximum => 255
164 validates_length_of :name, :maximum => 255
165 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
165 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
166 validate :validate_query_filters
166 validate :validate_query_filters
167 validate do |query|
167 validate do |query|
168 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
168 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
169 end
169 end
170
170
171 after_save do |query|
171 after_save do |query|
172 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
172 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
173 query.roles.clear
173 query.roles.clear
174 end
174 end
175 end
175 end
176
176
177 class_attribute :operators
177 class_attribute :operators
178 self.operators = {
178 self.operators = {
179 "=" => :label_equals,
179 "=" => :label_equals,
180 "!" => :label_not_equals,
180 "!" => :label_not_equals,
181 "o" => :label_open_issues,
181 "o" => :label_open_issues,
182 "c" => :label_closed_issues,
182 "c" => :label_closed_issues,
183 "!*" => :label_none,
183 "!*" => :label_none,
184 "*" => :label_any,
184 "*" => :label_any,
185 ">=" => :label_greater_or_equal,
185 ">=" => :label_greater_or_equal,
186 "<=" => :label_less_or_equal,
186 "<=" => :label_less_or_equal,
187 "><" => :label_between,
187 "><" => :label_between,
188 "<t+" => :label_in_less_than,
188 "<t+" => :label_in_less_than,
189 ">t+" => :label_in_more_than,
189 ">t+" => :label_in_more_than,
190 "><t+"=> :label_in_the_next_days,
190 "><t+"=> :label_in_the_next_days,
191 "t+" => :label_in,
191 "t+" => :label_in,
192 "t" => :label_today,
192 "t" => :label_today,
193 "ld" => :label_yesterday,
193 "ld" => :label_yesterday,
194 "w" => :label_this_week,
194 "w" => :label_this_week,
195 "lw" => :label_last_week,
195 "lw" => :label_last_week,
196 "l2w" => [:label_last_n_weeks, {:count => 2}],
196 "l2w" => [:label_last_n_weeks, {:count => 2}],
197 "m" => :label_this_month,
197 "m" => :label_this_month,
198 "lm" => :label_last_month,
198 "lm" => :label_last_month,
199 "y" => :label_this_year,
199 "y" => :label_this_year,
200 ">t-" => :label_less_than_ago,
200 ">t-" => :label_less_than_ago,
201 "<t-" => :label_more_than_ago,
201 "<t-" => :label_more_than_ago,
202 "><t-"=> :label_in_the_past_days,
202 "><t-"=> :label_in_the_past_days,
203 "t-" => :label_ago,
203 "t-" => :label_ago,
204 "~" => :label_contains,
204 "~" => :label_contains,
205 "!~" => :label_not_contains,
205 "!~" => :label_not_contains,
206 "=p" => :label_any_issues_in_project,
206 "=p" => :label_any_issues_in_project,
207 "=!p" => :label_any_issues_not_in_project,
207 "=!p" => :label_any_issues_not_in_project,
208 "!p" => :label_no_issues_in_project,
208 "!p" => :label_no_issues_in_project,
209 "*o" => :label_any_open_issues,
209 "*o" => :label_any_open_issues,
210 "!o" => :label_no_open_issues
210 "!o" => :label_no_open_issues
211 }
211 }
212
212
213 class_attribute :operators_by_filter_type
213 class_attribute :operators_by_filter_type
214 self.operators_by_filter_type = {
214 self.operators_by_filter_type = {
215 :list => [ "=", "!" ],
215 :list => [ "=", "!" ],
216 :list_status => [ "o", "=", "!", "c", "*" ],
216 :list_status => [ "o", "=", "!", "c", "*" ],
217 :list_optional => [ "=", "!", "!*", "*" ],
217 :list_optional => [ "=", "!", "!*", "*" ],
218 :list_subprojects => [ "*", "!*", "=" ],
218 :list_subprojects => [ "*", "!*", "=" ],
219 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
219 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
220 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
220 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
221 :string => [ "=", "~", "!", "!~", "!*", "*" ],
221 :string => [ "=", "~", "!", "!~", "!*", "*" ],
222 :text => [ "~", "!~", "!*", "*" ],
222 :text => [ "~", "!~", "!*", "*" ],
223 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
223 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
224 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
224 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
225 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
225 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
226 :tree => ["=", "~", "!*", "*"]
226 :tree => ["=", "~", "!*", "*"]
227 }
227 }
228
228
229 class_attribute :available_columns
229 class_attribute :available_columns
230 self.available_columns = []
230 self.available_columns = []
231
231
232 class_attribute :queried_class
232 class_attribute :queried_class
233
233
234 # Permission required to view the queries, set on subclasses.
234 # Permission required to view the queries, set on subclasses.
235 class_attribute :view_permission
235 class_attribute :view_permission
236
236
237 # Scope of queries that are global or on the given project
237 # Scope of queries that are global or on the given project
238 scope :global_or_on_project, lambda {|project|
238 scope :global_or_on_project, lambda {|project|
239 where(:project_id => (project.nil? ? nil : [nil, project.id]))
239 where(:project_id => (project.nil? ? nil : [nil, project.id]))
240 }
240 }
241
241
242 scope :sorted, lambda {order(:name, :id)}
242 scope :sorted, lambda {order(:name, :id)}
243
243
244 # Scope of visible queries, can be used from subclasses only.
244 # Scope of visible queries, can be used from subclasses only.
245 # Unlike other visible scopes, a class methods is used as it
245 # Unlike other visible scopes, a class methods is used as it
246 # let handle inheritance more nicely than scope DSL.
246 # let handle inheritance more nicely than scope DSL.
247 def self.visible(*args)
247 def self.visible(*args)
248 if self == ::Query
248 if self == ::Query
249 # Visibility depends on permissions for each subclass,
249 # Visibility depends on permissions for each subclass,
250 # raise an error if the scope is called from Query (eg. Query.visible)
250 # raise an error if the scope is called from Query (eg. Query.visible)
251 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
251 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
252 end
252 end
253
253
254 user = args.shift || User.current
254 user = args.shift || User.current
255 base = Project.allowed_to_condition(user, view_permission, *args)
255 base = Project.allowed_to_condition(user, view_permission, *args)
256 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
256 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
257 where("#{table_name}.project_id IS NULL OR (#{base})")
257 where("#{table_name}.project_id IS NULL OR (#{base})")
258
258
259 if user.admin?
259 if user.admin?
260 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
260 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
261 elsif user.memberships.any?
261 elsif user.memberships.any?
262 scope.where("#{table_name}.visibility = ?" +
262 scope.where("#{table_name}.visibility = ?" +
263 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
263 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
264 "SELECT DISTINCT q.id FROM #{table_name} q" +
264 "SELECT DISTINCT q.id FROM #{table_name} q" +
265 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
265 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
266 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
266 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
267 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
267 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
268 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
268 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
269 " OR #{table_name}.user_id = ?",
269 " OR #{table_name}.user_id = ?",
270 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
270 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
271 elsif user.logged?
271 elsif user.logged?
272 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
272 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
273 else
273 else
274 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
274 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
275 end
275 end
276 end
276 end
277
277
278 # Returns true if the query is visible to +user+ or the current user.
278 # Returns true if the query is visible to +user+ or the current user.
279 def visible?(user=User.current)
279 def visible?(user=User.current)
280 return true if user.admin?
280 return true if user.admin?
281 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
281 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
282 case visibility
282 case visibility
283 when VISIBILITY_PUBLIC
283 when VISIBILITY_PUBLIC
284 true
284 true
285 when VISIBILITY_ROLES
285 when VISIBILITY_ROLES
286 if project
286 if project
287 (user.roles_for_project(project) & roles).any?
287 (user.roles_for_project(project) & roles).any?
288 else
288 else
289 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
289 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
290 end
290 end
291 else
291 else
292 user == self.user
292 user == self.user
293 end
293 end
294 end
294 end
295
295
296 def is_private?
296 def is_private?
297 visibility == VISIBILITY_PRIVATE
297 visibility == VISIBILITY_PRIVATE
298 end
298 end
299
299
300 def is_public?
300 def is_public?
301 !is_private?
301 !is_private?
302 end
302 end
303
303
304 def queried_table_name
304 def queried_table_name
305 @queried_table_name ||= self.class.queried_class.table_name
305 @queried_table_name ||= self.class.queried_class.table_name
306 end
306 end
307
307
308 def initialize(attributes=nil, *args)
308 def initialize(attributes=nil, *args)
309 super attributes
309 super attributes
310 @is_for_all = project.nil?
310 @is_for_all = project.nil?
311 end
311 end
312
312
313 # Builds the query from the given params
313 # Builds the query from the given params
314 def build_from_params(params)
314 def build_from_params(params)
315 if params[:fields] || params[:f]
315 if params[:fields] || params[:f]
316 self.filters = {}
316 self.filters = {}
317 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
317 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
318 else
318 else
319 available_filters.keys.each do |field|
319 available_filters.keys.each do |field|
320 add_short_filter(field, params[field]) if params[field]
320 add_short_filter(field, params[field]) if params[field]
321 end
321 end
322 end
322 end
323 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
323 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
324 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
324 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
325 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
325 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
326 self
326 self
327 end
327 end
328
328
329 # Builds a new query from the given params and attributes
329 # Builds a new query from the given params and attributes
330 def self.build_from_params(params, attributes={})
330 def self.build_from_params(params, attributes={})
331 new(attributes).build_from_params(params)
331 new(attributes).build_from_params(params)
332 end
332 end
333
333
334 def validate_query_filters
334 def validate_query_filters
335 filters.each_key do |field|
335 filters.each_key do |field|
336 if values_for(field)
336 if values_for(field)
337 case type_for(field)
337 case type_for(field)
338 when :integer
338 when :integer
339 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
339 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
340 when :float
340 when :float
341 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
341 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
342 when :date, :date_past
342 when :date, :date_past
343 case operator_for(field)
343 case operator_for(field)
344 when "=", ">=", "<=", "><"
344 when "=", ">=", "<=", "><"
345 add_filter_error(field, :invalid) if values_for(field).detect {|v|
345 add_filter_error(field, :invalid) if values_for(field).detect {|v|
346 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
346 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
347 }
347 }
348 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
348 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
349 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
349 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
350 end
350 end
351 end
351 end
352 end
352 end
353
353
354 add_filter_error(field, :blank) unless
354 add_filter_error(field, :blank) unless
355 # filter requires one or more values
355 # filter requires one or more values
356 (values_for(field) and !values_for(field).first.blank?) or
356 (values_for(field) and !values_for(field).first.blank?) or
357 # filter doesn't require any value
357 # filter doesn't require any value
358 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
358 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
359 end if filters
359 end if filters
360 end
360 end
361
361
362 def add_filter_error(field, message)
362 def add_filter_error(field, message)
363 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
363 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
364 errors.add(:base, m)
364 errors.add(:base, m)
365 end
365 end
366
366
367 def editable_by?(user)
367 def editable_by?(user)
368 return false unless user
368 return false unless user
369 # Admin can edit them all and regular users can edit their private queries
369 # Admin can edit them all and regular users can edit their private queries
370 return true if user.admin? || (is_private? && self.user_id == user.id)
370 return true if user.admin? || (is_private? && self.user_id == user.id)
371 # Members can not edit public queries that are for all project (only admin is allowed to)
371 # Members can not edit public queries that are for all project (only admin is allowed to)
372 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
372 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
373 end
373 end
374
374
375 def trackers
375 def trackers
376 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
376 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
377 end
377 end
378
378
379 # Returns a hash of localized labels for all filter operators
379 # Returns a hash of localized labels for all filter operators
380 def self.operators_labels
380 def self.operators_labels
381 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
381 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
382 end
382 end
383
383
384 # Returns a representation of the available filters for JSON serialization
384 # Returns a representation of the available filters for JSON serialization
385 def available_filters_as_json
385 def available_filters_as_json
386 json = {}
386 json = {}
387 available_filters.each do |field, options|
387 available_filters.each do |field, options|
388 options = options.slice(:type, :name, :values)
388 options = options.slice(:type, :name, :values)
389 if options[:values] && values_for(field)
389 if options[:values] && values_for(field)
390 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
390 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
391 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
391 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
392 options[:values] += send(method, missing)
392 options[:values] += send(method, missing)
393 end
393 end
394 end
394 end
395 json[field] = options.stringify_keys
395 json[field] = options.stringify_keys
396 end
396 end
397 json
397 json
398 end
398 end
399
399
400 def all_projects
400 def all_projects
401 @all_projects ||= Project.visible.to_a
401 @all_projects ||= Project.visible.to_a
402 end
402 end
403
403
404 def all_projects_values
404 def all_projects_values
405 return @all_projects_values if @all_projects_values
405 return @all_projects_values if @all_projects_values
406
406
407 values = []
407 values = []
408 Project.project_tree(all_projects) do |p, level|
408 Project.project_tree(all_projects) do |p, level|
409 prefix = (level > 0 ? ('--' * level + ' ') : '')
409 prefix = (level > 0 ? ('--' * level + ' ') : '')
410 values << ["#{prefix}#{p.name}", p.id.to_s]
410 values << ["#{prefix}#{p.name}", p.id.to_s]
411 end
411 end
412 @all_projects_values = values
412 @all_projects_values = values
413 end
413 end
414
414
415 # Adds available filters
415 # Adds available filters
416 def initialize_available_filters
416 def initialize_available_filters
417 # implemented by sub-classes
417 # implemented by sub-classes
418 end
418 end
419 protected :initialize_available_filters
419 protected :initialize_available_filters
420
420
421 # Adds an available filter
421 # Adds an available filter
422 def add_available_filter(field, options)
422 def add_available_filter(field, options)
423 @available_filters ||= ActiveSupport::OrderedHash.new
423 @available_filters ||= ActiveSupport::OrderedHash.new
424 @available_filters[field] = options
424 @available_filters[field] = options
425 @available_filters
425 @available_filters
426 end
426 end
427
427
428 # Removes an available filter
428 # Removes an available filter
429 def delete_available_filter(field)
429 def delete_available_filter(field)
430 if @available_filters
430 if @available_filters
431 @available_filters.delete(field)
431 @available_filters.delete(field)
432 end
432 end
433 end
433 end
434
434
435 # Return a hash of available filters
435 # Return a hash of available filters
436 def available_filters
436 def available_filters
437 unless @available_filters
437 unless @available_filters
438 initialize_available_filters
438 initialize_available_filters
439 @available_filters.each do |field, options|
439 @available_filters.each do |field, options|
440 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
440 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
441 end
441 end
442 end
442 end
443 @available_filters
443 @available_filters
444 end
444 end
445
445
446 def add_filter(field, operator, values=nil)
446 def add_filter(field, operator, values=nil)
447 # values must be an array
447 # values must be an array
448 return unless values.nil? || values.is_a?(Array)
448 return unless values.nil? || values.is_a?(Array)
449 # check if field is defined as an available filter
449 # check if field is defined as an available filter
450 if available_filters.has_key? field
450 if available_filters.has_key? field
451 filter_options = available_filters[field]
451 filter_options = available_filters[field]
452 filters[field] = {:operator => operator, :values => (values || [''])}
452 filters[field] = {:operator => operator, :values => (values || [''])}
453 end
453 end
454 end
454 end
455
455
456 def add_short_filter(field, expression)
456 def add_short_filter(field, expression)
457 return unless expression && available_filters.has_key?(field)
457 return unless expression && available_filters.has_key?(field)
458 field_type = available_filters[field][:type]
458 field_type = available_filters[field][:type]
459 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
459 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
460 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
460 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
461 values = $1
461 values = $1
462 add_filter field, operator, values.present? ? values.split('|') : ['']
462 add_filter field, operator, values.present? ? values.split('|') : ['']
463 end || add_filter(field, '=', expression.to_s.split('|'))
463 end || add_filter(field, '=', expression.to_s.split('|'))
464 end
464 end
465
465
466 # Add multiple filters using +add_filter+
466 # Add multiple filters using +add_filter+
467 def add_filters(fields, operators, values)
467 def add_filters(fields, operators, values)
468 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
468 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
469 fields.each do |field|
469 fields.each do |field|
470 add_filter(field, operators[field], values && values[field])
470 add_filter(field, operators[field], values && values[field])
471 end
471 end
472 end
472 end
473 end
473 end
474
474
475 def has_filter?(field)
475 def has_filter?(field)
476 filters and filters[field]
476 filters and filters[field]
477 end
477 end
478
478
479 def type_for(field)
479 def type_for(field)
480 available_filters[field][:type] if available_filters.has_key?(field)
480 available_filters[field][:type] if available_filters.has_key?(field)
481 end
481 end
482
482
483 def operator_for(field)
483 def operator_for(field)
484 has_filter?(field) ? filters[field][:operator] : nil
484 has_filter?(field) ? filters[field][:operator] : nil
485 end
485 end
486
486
487 def values_for(field)
487 def values_for(field)
488 has_filter?(field) ? filters[field][:values] : nil
488 has_filter?(field) ? filters[field][:values] : nil
489 end
489 end
490
490
491 def value_for(field, index=0)
491 def value_for(field, index=0)
492 (values_for(field) || [])[index]
492 (values_for(field) || [])[index]
493 end
493 end
494
494
495 def label_for(field)
495 def label_for(field)
496 label = available_filters[field][:name] if available_filters.has_key?(field)
496 label = available_filters[field][:name] if available_filters.has_key?(field)
497 label ||= queried_class.human_attribute_name(field, :default => field)
497 label ||= queried_class.human_attribute_name(field, :default => field)
498 end
498 end
499
499
500 def self.add_available_column(column)
500 def self.add_available_column(column)
501 self.available_columns << (column) if column.is_a?(QueryColumn)
501 self.available_columns << (column) if column.is_a?(QueryColumn)
502 end
502 end
503
503
504 # Returns an array of columns that can be used to group the results
504 # Returns an array of columns that can be used to group the results
505 def groupable_columns
505 def groupable_columns
506 available_columns.select {|c| c.groupable}
506 available_columns.select {|c| c.groupable}
507 end
507 end
508
508
509 # Returns a Hash of columns and the key for sorting
509 # Returns a Hash of columns and the key for sorting
510 def sortable_columns
510 def sortable_columns
511 available_columns.inject({}) {|h, column|
511 available_columns.inject({}) {|h, column|
512 h[column.name.to_s] = column.sortable
512 h[column.name.to_s] = column.sortable
513 h
513 h
514 }
514 }
515 end
515 end
516
516
517 def columns
517 def columns
518 # preserve the column_names order
518 # preserve the column_names order
519 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
519 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
520 available_columns.find { |col| col.name == name }
520 available_columns.find { |col| col.name == name }
521 end.compact
521 end.compact
522 available_columns.select(&:frozen?) | cols
522 available_columns.select(&:frozen?) | cols
523 end
523 end
524
524
525 def inline_columns
525 def inline_columns
526 columns.select(&:inline?)
526 columns.select(&:inline?)
527 end
527 end
528
528
529 def block_columns
529 def block_columns
530 columns.reject(&:inline?)
530 columns.reject(&:inline?)
531 end
531 end
532
532
533 def available_inline_columns
533 def available_inline_columns
534 available_columns.select(&:inline?)
534 available_columns.select(&:inline?)
535 end
535 end
536
536
537 def available_block_columns
537 def available_block_columns
538 available_columns.reject(&:inline?)
538 available_columns.reject(&:inline?)
539 end
539 end
540
540
541 def available_totalable_columns
541 def available_totalable_columns
542 available_columns.select(&:totalable)
542 available_columns.select(&:totalable)
543 end
543 end
544
544
545 def default_columns_names
545 def default_columns_names
546 []
546 []
547 end
547 end
548
548
549 def column_names=(names)
549 def column_names=(names)
550 if names
550 if names
551 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
551 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
552 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
552 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
553 # Set column_names to nil if default columns
553 # Set column_names to nil if default columns
554 if names == default_columns_names
554 if names == default_columns_names
555 names = nil
555 names = nil
556 end
556 end
557 end
557 end
558 write_attribute(:column_names, names)
558 write_attribute(:column_names, names)
559 end
559 end
560
560
561 def has_column?(column)
561 def has_column?(column)
562 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
562 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
563 end
563 end
564
564
565 def has_custom_field_column?
565 def has_custom_field_column?
566 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
566 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
567 end
567 end
568
568
569 def has_default_columns?
569 def has_default_columns?
570 column_names.nil? || column_names.empty?
570 column_names.nil? || column_names.empty?
571 end
571 end
572
572
573 def totalable_columns
573 def totalable_columns
574 names = totalable_names
574 names = totalable_names
575 available_totalable_columns.select {|column| names.include?(column.name)}
575 available_totalable_columns.select {|column| names.include?(column.name)}
576 end
576 end
577
577
578 def totalable_names=(names)
578 def totalable_names=(names)
579 if names
579 if names
580 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
580 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
581 end
581 end
582 options[:totalable_names] = names
582 options[:totalable_names] = names
583 end
583 end
584
584
585 def totalable_names
585 def totalable_names
586 options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || []
586 options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || []
587 end
587 end
588
588
589 def sort_criteria=(arg)
589 def sort_criteria=(arg)
590 c = []
590 c = []
591 if arg.is_a?(Hash)
591 if arg.is_a?(Hash)
592 arg = arg.keys.sort.collect {|k| arg[k]}
592 arg = arg.keys.sort.collect {|k| arg[k]}
593 end
593 end
594 if arg
594 if arg
595 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
595 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
596 end
596 end
597 write_attribute(:sort_criteria, c)
597 write_attribute(:sort_criteria, c)
598 end
598 end
599
599
600 def sort_criteria
600 def sort_criteria
601 read_attribute(:sort_criteria) || []
601 read_attribute(:sort_criteria) || []
602 end
602 end
603
603
604 def sort_criteria_key(arg)
604 def sort_criteria_key(arg)
605 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
605 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
606 end
606 end
607
607
608 def sort_criteria_order(arg)
608 def sort_criteria_order(arg)
609 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
609 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
610 end
610 end
611
611
612 def sort_criteria_order_for(key)
612 def sort_criteria_order_for(key)
613 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
613 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
614 end
614 end
615
615
616 # Returns the SQL sort order that should be prepended for grouping
616 # Returns the SQL sort order that should be prepended for grouping
617 def group_by_sort_order
617 def group_by_sort_order
618 if column = group_by_column
618 if column = group_by_column
619 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
619 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
620 Array(column.sortable).map {|s| "#{s} #{order}"}
620 Array(column.sortable).map {|s| "#{s} #{order}"}
621 end
621 end
622 end
622 end
623
623
624 # Returns true if the query is a grouped query
624 # Returns true if the query is a grouped query
625 def grouped?
625 def grouped?
626 !group_by_column.nil?
626 !group_by_column.nil?
627 end
627 end
628
628
629 def group_by_column
629 def group_by_column
630 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
630 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
631 end
631 end
632
632
633 def group_by_statement
633 def group_by_statement
634 group_by_column.try(:groupable)
634 group_by_column.try(:groupable)
635 end
635 end
636
636
637 def project_statement
637 def project_statement
638 project_clauses = []
638 project_clauses = []
639 if project && !project.descendants.active.empty?
639 if project && !project.descendants.active.empty?
640 if has_filter?("subproject_id")
640 if has_filter?("subproject_id")
641 case operator_for("subproject_id")
641 case operator_for("subproject_id")
642 when '='
642 when '='
643 # include the selected subprojects
643 # include the selected subprojects
644 ids = [project.id] + values_for("subproject_id").each(&:to_i)
644 ids = [project.id] + values_for("subproject_id").each(&:to_i)
645 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
645 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
646 when '!*'
646 when '!*'
647 # main project only
647 # main project only
648 project_clauses << "#{Project.table_name}.id = %d" % project.id
648 project_clauses << "#{Project.table_name}.id = %d" % project.id
649 else
649 else
650 # all subprojects
650 # all subprojects
651 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
651 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
652 end
652 end
653 elsif Setting.display_subprojects_issues?
653 elsif Setting.display_subprojects_issues?
654 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
654 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
655 else
655 else
656 project_clauses << "#{Project.table_name}.id = %d" % project.id
656 project_clauses << "#{Project.table_name}.id = %d" % project.id
657 end
657 end
658 elsif project
658 elsif project
659 project_clauses << "#{Project.table_name}.id = %d" % project.id
659 project_clauses << "#{Project.table_name}.id = %d" % project.id
660 end
660 end
661 project_clauses.any? ? project_clauses.join(' AND ') : nil
661 project_clauses.any? ? project_clauses.join(' AND ') : nil
662 end
662 end
663
663
664 def statement
664 def statement
665 # filters clauses
665 # filters clauses
666 filters_clauses = []
666 filters_clauses = []
667 filters.each_key do |field|
667 filters.each_key do |field|
668 next if field == "subproject_id"
668 next if field == "subproject_id"
669 v = values_for(field).clone
669 v = values_for(field).clone
670 next unless v and !v.empty?
670 next unless v and !v.empty?
671 operator = operator_for(field)
671 operator = operator_for(field)
672
672
673 # "me" value substitution
673 # "me" value substitution
674 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
674 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
675 if v.delete("me")
675 if v.delete("me")
676 if User.current.logged?
676 if User.current.logged?
677 v.push(User.current.id.to_s)
677 v.push(User.current.id.to_s)
678 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
678 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
679 else
679 else
680 v.push("0")
680 v.push("0")
681 end
681 end
682 end
682 end
683 end
683 end
684
684
685 if field == 'project_id'
685 if field == 'project_id'
686 if v.delete('mine')
686 if v.delete('mine')
687 v += User.current.memberships.map(&:project_id).map(&:to_s)
687 v += User.current.memberships.map(&:project_id).map(&:to_s)
688 end
688 end
689 end
689 end
690
690
691 if field =~ /cf_(\d+)$/
691 if field =~ /cf_(\d+)$/
692 # custom field
692 # custom field
693 filters_clauses << sql_for_custom_field(field, operator, v, $1)
693 filters_clauses << sql_for_custom_field(field, operator, v, $1)
694 elsif respond_to?("sql_for_#{field}_field")
694 elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
695 # specific statement
695 # specific statement
696 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
696 filters_clauses << send(method, field, operator, v)
697 else
697 else
698 # regular field
698 # regular field
699 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
699 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
700 end
700 end
701 end if filters and valid?
701 end if filters and valid?
702
702
703 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
703 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
704 # Excludes results for which the grouped custom field is not visible
704 # Excludes results for which the grouped custom field is not visible
705 filters_clauses << c.custom_field.visibility_by_project_condition
705 filters_clauses << c.custom_field.visibility_by_project_condition
706 end
706 end
707
707
708 filters_clauses << project_statement
708 filters_clauses << project_statement
709 filters_clauses.reject!(&:blank?)
709 filters_clauses.reject!(&:blank?)
710
710
711 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
711 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
712 end
712 end
713
713
714 # Returns the sum of values for the given column
714 # Returns the sum of values for the given column
715 def total_for(column)
715 def total_for(column)
716 total_with_scope(column, base_scope)
716 total_with_scope(column, base_scope)
717 end
717 end
718
718
719 # Returns a hash of the sum of the given column for each group,
719 # Returns a hash of the sum of the given column for each group,
720 # or nil if the query is not grouped
720 # or nil if the query is not grouped
721 def total_by_group_for(column)
721 def total_by_group_for(column)
722 grouped_query do |scope|
722 grouped_query do |scope|
723 total_with_scope(column, scope)
723 total_with_scope(column, scope)
724 end
724 end
725 end
725 end
726
726
727 def totals
727 def totals
728 totals = totalable_columns.map {|column| [column, total_for(column)]}
728 totals = totalable_columns.map {|column| [column, total_for(column)]}
729 yield totals if block_given?
729 yield totals if block_given?
730 totals
730 totals
731 end
731 end
732
732
733 def totals_by_group
733 def totals_by_group
734 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
734 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
735 yield totals if block_given?
735 yield totals if block_given?
736 totals
736 totals
737 end
737 end
738
738
739 private
739 private
740
740
741 def grouped_query(&block)
741 def grouped_query(&block)
742 r = nil
742 r = nil
743 if grouped?
743 if grouped?
744 begin
744 begin
745 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
745 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
746 r = yield base_group_scope
746 r = yield base_group_scope
747 rescue ActiveRecord::RecordNotFound
747 rescue ActiveRecord::RecordNotFound
748 r = {nil => yield(base_scope)}
748 r = {nil => yield(base_scope)}
749 end
749 end
750 c = group_by_column
750 c = group_by_column
751 if c.is_a?(QueryCustomFieldColumn)
751 if c.is_a?(QueryCustomFieldColumn)
752 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
752 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
753 end
753 end
754 end
754 end
755 r
755 r
756 rescue ::ActiveRecord::StatementInvalid => e
756 rescue ::ActiveRecord::StatementInvalid => e
757 raise StatementInvalid.new(e.message)
757 raise StatementInvalid.new(e.message)
758 end
758 end
759
759
760 def total_with_scope(column, scope)
760 def total_with_scope(column, scope)
761 unless column.is_a?(QueryColumn)
761 unless column.is_a?(QueryColumn)
762 column = column.to_sym
762 column = column.to_sym
763 column = available_totalable_columns.detect {|c| c.name == column}
763 column = available_totalable_columns.detect {|c| c.name == column}
764 end
764 end
765 if column.is_a?(QueryCustomFieldColumn)
765 if column.is_a?(QueryCustomFieldColumn)
766 custom_field = column.custom_field
766 custom_field = column.custom_field
767 send "total_for_custom_field", custom_field, scope
767 send "total_for_custom_field", custom_field, scope
768 else
768 else
769 send "total_for_#{column.name}", scope
769 send "total_for_#{column.name}", scope
770 end
770 end
771 rescue ::ActiveRecord::StatementInvalid => e
771 rescue ::ActiveRecord::StatementInvalid => e
772 raise StatementInvalid.new(e.message)
772 raise StatementInvalid.new(e.message)
773 end
773 end
774
774
775 def base_scope
775 def base_scope
776 raise "unimplemented"
776 raise "unimplemented"
777 end
777 end
778
778
779 def base_group_scope
779 def base_group_scope
780 base_scope.
780 base_scope.
781 joins(joins_for_order_statement(group_by_statement)).
781 joins(joins_for_order_statement(group_by_statement)).
782 group(group_by_statement)
782 group(group_by_statement)
783 end
783 end
784
784
785 def total_for_custom_field(custom_field, scope, &block)
785 def total_for_custom_field(custom_field, scope, &block)
786 total = custom_field.format.total_for_scope(custom_field, scope)
786 total = custom_field.format.total_for_scope(custom_field, scope)
787 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
787 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
788 total
788 total
789 end
789 end
790
790
791 def map_total(total, &block)
791 def map_total(total, &block)
792 if total.is_a?(Hash)
792 if total.is_a?(Hash)
793 total.keys.each {|k| total[k] = yield total[k]}
793 total.keys.each {|k| total[k] = yield total[k]}
794 else
794 else
795 total = yield total
795 total = yield total
796 end
796 end
797 total
797 total
798 end
798 end
799
799
800 def sql_for_custom_field(field, operator, value, custom_field_id)
800 def sql_for_custom_field(field, operator, value, custom_field_id)
801 db_table = CustomValue.table_name
801 db_table = CustomValue.table_name
802 db_field = 'value'
802 db_field = 'value'
803 filter = @available_filters[field]
803 filter = @available_filters[field]
804 return nil unless filter
804 return nil unless filter
805 if filter[:field].format.target_class && filter[:field].format.target_class <= User
805 if filter[:field].format.target_class && filter[:field].format.target_class <= User
806 if value.delete('me')
806 if value.delete('me')
807 value.push User.current.id.to_s
807 value.push User.current.id.to_s
808 end
808 end
809 end
809 end
810 not_in = nil
810 not_in = nil
811 if operator == '!'
811 if operator == '!'
812 # Makes ! operator work for custom fields with multiple values
812 # Makes ! operator work for custom fields with multiple values
813 operator = '='
813 operator = '='
814 not_in = 'NOT'
814 not_in = 'NOT'
815 end
815 end
816 customized_key = "id"
816 customized_key = "id"
817 customized_class = queried_class
817 customized_class = queried_class
818 if field =~ /^(.+)\.cf_/
818 if field =~ /^(.+)\.cf_/
819 assoc = $1
819 assoc = $1
820 customized_key = "#{assoc}_id"
820 customized_key = "#{assoc}_id"
821 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
821 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
822 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
822 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
823 end
823 end
824 where = sql_for_field(field, operator, value, db_table, db_field, true)
824 where = sql_for_field(field, operator, value, db_table, db_field, true)
825 if operator =~ /[<>]/
825 if operator =~ /[<>]/
826 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
826 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
827 end
827 end
828 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
828 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
829 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
829 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
830 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
830 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
831 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
831 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
832 end
832 end
833
833
834 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
834 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
835 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
835 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
836 sql = ''
836 sql = ''
837 case operator
837 case operator
838 when "="
838 when "="
839 if value.any?
839 if value.any?
840 case type_for(field)
840 case type_for(field)
841 when :date, :date_past
841 when :date, :date_past
842 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
842 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
843 when :integer
843 when :integer
844 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
844 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
845 if int_values.present?
845 if int_values.present?
846 if is_custom_filter
846 if is_custom_filter
847 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) IN (#{int_values}))"
847 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) IN (#{int_values}))"
848 else
848 else
849 sql = "#{db_table}.#{db_field} IN (#{int_values})"
849 sql = "#{db_table}.#{db_field} IN (#{int_values})"
850 end
850 end
851 else
851 else
852 sql = "1=0"
852 sql = "1=0"
853 end
853 end
854 when :float
854 when :float
855 if is_custom_filter
855 if is_custom_filter
856 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
856 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
857 else
857 else
858 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
858 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
859 end
859 end
860 else
860 else
861 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
861 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
862 end
862 end
863 else
863 else
864 # IN an empty set
864 # IN an empty set
865 sql = "1=0"
865 sql = "1=0"
866 end
866 end
867 when "!"
867 when "!"
868 if value.any?
868 if value.any?
869 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
869 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
870 else
870 else
871 # NOT IN an empty set
871 # NOT IN an empty set
872 sql = "1=1"
872 sql = "1=1"
873 end
873 end
874 when "!*"
874 when "!*"
875 sql = "#{db_table}.#{db_field} IS NULL"
875 sql = "#{db_table}.#{db_field} IS NULL"
876 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
876 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
877 when "*"
877 when "*"
878 sql = "#{db_table}.#{db_field} IS NOT NULL"
878 sql = "#{db_table}.#{db_field} IS NOT NULL"
879 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
879 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
880 when ">="
880 when ">="
881 if [:date, :date_past].include?(type_for(field))
881 if [:date, :date_past].include?(type_for(field))
882 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
882 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
883 else
883 else
884 if is_custom_filter
884 if is_custom_filter
885 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
885 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
886 else
886 else
887 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
887 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
888 end
888 end
889 end
889 end
890 when "<="
890 when "<="
891 if [:date, :date_past].include?(type_for(field))
891 if [:date, :date_past].include?(type_for(field))
892 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
892 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
893 else
893 else
894 if is_custom_filter
894 if is_custom_filter
895 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
895 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
896 else
896 else
897 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
897 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
898 end
898 end
899 end
899 end
900 when "><"
900 when "><"
901 if [:date, :date_past].include?(type_for(field))
901 if [:date, :date_past].include?(type_for(field))
902 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
902 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
903 else
903 else
904 if is_custom_filter
904 if is_custom_filter
905 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
905 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
906 else
906 else
907 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
907 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
908 end
908 end
909 end
909 end
910 when "o"
910 when "o"
911 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
911 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
912 when "c"
912 when "c"
913 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
913 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
914 when "><t-"
914 when "><t-"
915 # between today - n days and today
915 # between today - n days and today
916 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
916 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
917 when ">t-"
917 when ">t-"
918 # >= today - n days
918 # >= today - n days
919 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
919 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
920 when "<t-"
920 when "<t-"
921 # <= today - n days
921 # <= today - n days
922 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
922 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
923 when "t-"
923 when "t-"
924 # = n days in past
924 # = n days in past
925 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
925 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
926 when "><t+"
926 when "><t+"
927 # between today and today + n days
927 # between today and today + n days
928 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
928 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
929 when ">t+"
929 when ">t+"
930 # >= today + n days
930 # >= today + n days
931 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
931 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
932 when "<t+"
932 when "<t+"
933 # <= today + n days
933 # <= today + n days
934 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
934 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
935 when "t+"
935 when "t+"
936 # = today + n days
936 # = today + n days
937 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
937 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
938 when "t"
938 when "t"
939 # = today
939 # = today
940 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
940 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
941 when "ld"
941 when "ld"
942 # = yesterday
942 # = yesterday
943 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
943 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
944 when "w"
944 when "w"
945 # = this week
945 # = this week
946 first_day_of_week = l(:general_first_day_of_week).to_i
946 first_day_of_week = l(:general_first_day_of_week).to_i
947 day_of_week = User.current.today.cwday
947 day_of_week = User.current.today.cwday
948 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
948 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
949 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
949 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
950 when "lw"
950 when "lw"
951 # = last week
951 # = last week
952 first_day_of_week = l(:general_first_day_of_week).to_i
952 first_day_of_week = l(:general_first_day_of_week).to_i
953 day_of_week = User.current.today.cwday
953 day_of_week = User.current.today.cwday
954 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
954 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
955 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
955 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
956 when "l2w"
956 when "l2w"
957 # = last 2 weeks
957 # = last 2 weeks
958 first_day_of_week = l(:general_first_day_of_week).to_i
958 first_day_of_week = l(:general_first_day_of_week).to_i
959 day_of_week = User.current.today.cwday
959 day_of_week = User.current.today.cwday
960 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
960 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
961 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
961 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
962 when "m"
962 when "m"
963 # = this month
963 # = this month
964 date = User.current.today
964 date = User.current.today
965 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
965 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
966 when "lm"
966 when "lm"
967 # = last month
967 # = last month
968 date = User.current.today.prev_month
968 date = User.current.today.prev_month
969 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
969 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
970 when "y"
970 when "y"
971 # = this year
971 # = this year
972 date = User.current.today
972 date = User.current.today
973 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
973 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
974 when "~"
974 when "~"
975 sql = sql_contains("#{db_table}.#{db_field}", value.first)
975 sql = sql_contains("#{db_table}.#{db_field}", value.first)
976 when "!~"
976 when "!~"
977 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
977 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
978 else
978 else
979 raise "Unknown query operator #{operator}"
979 raise "Unknown query operator #{operator}"
980 end
980 end
981
981
982 return sql
982 return sql
983 end
983 end
984
984
985 # Returns a SQL LIKE statement with wildcards
985 # Returns a SQL LIKE statement with wildcards
986 def sql_contains(db_field, value, match=true)
986 def sql_contains(db_field, value, match=true)
987 queried_class.send :sanitize_sql_for_conditions,
987 queried_class.send :sanitize_sql_for_conditions,
988 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
988 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
989 end
989 end
990
990
991 # Adds a filter for the given custom field
991 # Adds a filter for the given custom field
992 def add_custom_field_filter(field, assoc=nil)
992 def add_custom_field_filter(field, assoc=nil)
993 options = field.query_filter_options(self)
993 options = field.query_filter_options(self)
994 if field.format.target_class && field.format.target_class <= User
994 if field.format.target_class && field.format.target_class <= User
995 if options[:values].is_a?(Array) && User.current.logged?
995 if options[:values].is_a?(Array) && User.current.logged?
996 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
996 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
997 end
997 end
998 end
998 end
999
999
1000 filter_id = "cf_#{field.id}"
1000 filter_id = "cf_#{field.id}"
1001 filter_name = field.name
1001 filter_name = field.name
1002 if assoc.present?
1002 if assoc.present?
1003 filter_id = "#{assoc}.#{filter_id}"
1003 filter_id = "#{assoc}.#{filter_id}"
1004 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1004 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1005 end
1005 end
1006 add_available_filter filter_id, options.merge({
1006 add_available_filter filter_id, options.merge({
1007 :name => filter_name,
1007 :name => filter_name,
1008 :field => field
1008 :field => field
1009 })
1009 })
1010 end
1010 end
1011
1011
1012 # Adds filters for the given custom fields scope
1012 # Adds filters for the given custom fields scope
1013 def add_custom_fields_filters(scope, assoc=nil)
1013 def add_custom_fields_filters(scope, assoc=nil)
1014 scope.visible.where(:is_filter => true).sorted.each do |field|
1014 scope.visible.where(:is_filter => true).sorted.each do |field|
1015 add_custom_field_filter(field, assoc)
1015 add_custom_field_filter(field, assoc)
1016 end
1016 end
1017 end
1017 end
1018
1018
1019 # Adds filters for the given associations custom fields
1019 # Adds filters for the given associations custom fields
1020 def add_associations_custom_fields_filters(*associations)
1020 def add_associations_custom_fields_filters(*associations)
1021 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1021 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1022 associations.each do |assoc|
1022 associations.each do |assoc|
1023 association_klass = queried_class.reflect_on_association(assoc).klass
1023 association_klass = queried_class.reflect_on_association(assoc).klass
1024 fields_by_class.each do |field_class, fields|
1024 fields_by_class.each do |field_class, fields|
1025 if field_class.customized_class <= association_klass
1025 if field_class.customized_class <= association_klass
1026 fields.sort.each do |field|
1026 fields.sort.each do |field|
1027 add_custom_field_filter(field, assoc)
1027 add_custom_field_filter(field, assoc)
1028 end
1028 end
1029 end
1029 end
1030 end
1030 end
1031 end
1031 end
1032 end
1032 end
1033
1033
1034 def quoted_time(time, is_custom_filter)
1034 def quoted_time(time, is_custom_filter)
1035 if is_custom_filter
1035 if is_custom_filter
1036 # Custom field values are stored as strings in the DB
1036 # Custom field values are stored as strings in the DB
1037 # using this format that does not depend on DB date representation
1037 # using this format that does not depend on DB date representation
1038 time.strftime("%Y-%m-%d %H:%M:%S")
1038 time.strftime("%Y-%m-%d %H:%M:%S")
1039 else
1039 else
1040 self.class.connection.quoted_date(time)
1040 self.class.connection.quoted_date(time)
1041 end
1041 end
1042 end
1042 end
1043
1043
1044 def date_for_user_time_zone(y, m, d)
1044 def date_for_user_time_zone(y, m, d)
1045 if tz = User.current.time_zone
1045 if tz = User.current.time_zone
1046 tz.local y, m, d
1046 tz.local y, m, d
1047 else
1047 else
1048 Time.local y, m, d
1048 Time.local y, m, d
1049 end
1049 end
1050 end
1050 end
1051
1051
1052 # Returns a SQL clause for a date or datetime field.
1052 # Returns a SQL clause for a date or datetime field.
1053 def date_clause(table, field, from, to, is_custom_filter)
1053 def date_clause(table, field, from, to, is_custom_filter)
1054 s = []
1054 s = []
1055 if from
1055 if from
1056 if from.is_a?(Date)
1056 if from.is_a?(Date)
1057 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1057 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1058 else
1058 else
1059 from = from - 1 # second
1059 from = from - 1 # second
1060 end
1060 end
1061 if self.class.default_timezone == :utc
1061 if self.class.default_timezone == :utc
1062 from = from.utc
1062 from = from.utc
1063 end
1063 end
1064 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1064 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1065 end
1065 end
1066 if to
1066 if to
1067 if to.is_a?(Date)
1067 if to.is_a?(Date)
1068 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1068 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1069 end
1069 end
1070 if self.class.default_timezone == :utc
1070 if self.class.default_timezone == :utc
1071 to = to.utc
1071 to = to.utc
1072 end
1072 end
1073 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1073 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1074 end
1074 end
1075 s.join(' AND ')
1075 s.join(' AND ')
1076 end
1076 end
1077
1077
1078 # Returns a SQL clause for a date or datetime field using relative dates.
1078 # Returns a SQL clause for a date or datetime field using relative dates.
1079 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1079 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1080 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1080 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1081 end
1081 end
1082
1082
1083 # Returns a Date or Time from the given filter value
1083 # Returns a Date or Time from the given filter value
1084 def parse_date(arg)
1084 def parse_date(arg)
1085 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1085 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1086 Time.parse(arg) rescue nil
1086 Time.parse(arg) rescue nil
1087 else
1087 else
1088 Date.parse(arg) rescue nil
1088 Date.parse(arg) rescue nil
1089 end
1089 end
1090 end
1090 end
1091
1091
1092 # Additional joins required for the given sort options
1092 # Additional joins required for the given sort options
1093 def joins_for_order_statement(order_options)
1093 def joins_for_order_statement(order_options)
1094 joins = []
1094 joins = []
1095
1095
1096 if order_options
1096 if order_options
1097 if order_options.include?('authors')
1097 if order_options.include?('authors')
1098 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1098 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1099 end
1099 end
1100 order_options.scan(/cf_\d+/).uniq.each do |name|
1100 order_options.scan(/cf_\d+/).uniq.each do |name|
1101 column = available_columns.detect {|c| c.name.to_s == name}
1101 column = available_columns.detect {|c| c.name.to_s == name}
1102 join = column && column.custom_field.join_for_order_statement
1102 join = column && column.custom_field.join_for_order_statement
1103 if join
1103 if join
1104 joins << join
1104 joins << join
1105 end
1105 end
1106 end
1106 end
1107 end
1107 end
1108
1108
1109 joins.any? ? joins.join(' ') : nil
1109 joins.any? ? joins.join(' ') : nil
1110 end
1110 end
1111 end
1111 end
@@ -1,163 +1,187
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 TimeEntryQuery < Query
18 class TimeEntryQuery < Query
19
19
20 self.queried_class = TimeEntry
20 self.queried_class = TimeEntry
21 self.view_permission = :view_time_entries
21 self.view_permission = :view_time_entries
22
22
23 self.available_columns = [
23 self.available_columns = [
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25 QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
25 QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
26 QueryColumn.new(:tweek, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :caption => l(:label_week)),
26 QueryColumn.new(:tweek, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :caption => l(:label_week)),
27 QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
27 QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
28 QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
28 QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
29 QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
29 QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
30 QueryColumn.new(:comments),
30 QueryColumn.new(:comments),
31 QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours"),
31 QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours"),
32 ]
32 ]
33
33
34 def initialize(attributes=nil, *args)
34 def initialize(attributes=nil, *args)
35 super attributes
35 super attributes
36 self.filters ||= {}
36 self.filters ||= {}
37 add_filter('spent_on', '*') unless filters.present?
37 add_filter('spent_on', '*') unless filters.present?
38 end
38 end
39
39
40 def initialize_available_filters
40 def initialize_available_filters
41 add_available_filter "spent_on", :type => :date_past
41 add_available_filter "spent_on", :type => :date_past
42
42
43 principals = []
43 principals = []
44 versions = []
44 if project
45 if project
45 principals += project.principals.visible.sort
46 principals += project.principals.visible.sort
46 unless project.leaf?
47 unless project.leaf?
47 subprojects = project.descendants.visible.to_a
48 subprojects = project.descendants.visible.to_a
48 if subprojects.any?
49 if subprojects.any?
49 add_available_filter "subproject_id",
50 add_available_filter "subproject_id",
50 :type => :list_subprojects,
51 :type => :list_subprojects,
51 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
52 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
52 principals += Principal.member_of(subprojects).visible
53 principals += Principal.member_of(subprojects).visible
53 end
54 end
54 end
55 end
56 versions = project.shared_versions.to_a
55 else
57 else
56 if all_projects.any?
58 if all_projects.any?
57 # members of visible projects
59 # members of visible projects
58 principals += Principal.member_of(all_projects).visible
60 principals += Principal.member_of(all_projects).visible
59 # project filter
61 # project filter
60 project_values = []
62 project_values = []
61 if User.current.logged? && User.current.memberships.any?
63 if User.current.logged? && User.current.memberships.any?
62 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
64 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
63 end
65 end
64 project_values += all_projects_values
66 project_values += all_projects_values
65 add_available_filter("project_id",
67 add_available_filter("project_id",
66 :type => :list, :values => project_values
68 :type => :list, :values => project_values
67 ) unless project_values.empty?
69 ) unless project_values.empty?
68 end
70 end
69 end
71 end
70
72
71 add_available_filter("issue_id", :type => :tree, :label => :label_issue)
73 add_available_filter("issue_id", :type => :tree, :label => :label_issue)
74 add_available_filter("issue.fixed_version_id",
75 :type => :list,
76 :name => l("label_attribute_of_issue", :name => l(:field_fixed_version)),
77 :values => Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] })
72
78
73 principals.uniq!
79 principals.uniq!
74 principals.sort!
80 principals.sort!
75 users = principals.select {|p| p.is_a?(User)}
81 users = principals.select {|p| p.is_a?(User)}
76
82
77 users_values = []
83 users_values = []
78 users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
84 users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
79 users_values += users.collect{|s| [s.name, s.id.to_s] }
85 users_values += users.collect{|s| [s.name, s.id.to_s] }
80 add_available_filter("user_id",
86 add_available_filter("user_id",
81 :type => :list_optional, :values => users_values
87 :type => :list_optional, :values => users_values
82 ) unless users_values.empty?
88 ) unless users_values.empty?
83
89
84 activities = (project ? project.activities : TimeEntryActivity.shared)
90 activities = (project ? project.activities : TimeEntryActivity.shared)
85 add_available_filter("activity_id",
91 add_available_filter("activity_id",
86 :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
92 :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
87 ) unless activities.empty?
93 ) unless activities.empty?
88
94
89 add_available_filter "comments", :type => :text
95 add_available_filter "comments", :type => :text
90 add_available_filter "hours", :type => :float
96 add_available_filter "hours", :type => :float
91
97
92 add_custom_fields_filters(TimeEntryCustomField)
98 add_custom_fields_filters(TimeEntryCustomField)
93 add_associations_custom_fields_filters :project, :issue, :user
99 add_associations_custom_fields_filters :project, :issue, :user
94 end
100 end
95
101
96 def available_columns
102 def available_columns
97 return @available_columns if @available_columns
103 return @available_columns if @available_columns
98 @available_columns = self.class.available_columns.dup
104 @available_columns = self.class.available_columns.dup
99 @available_columns += TimeEntryCustomField.visible.
105 @available_columns += TimeEntryCustomField.visible.
100 map {|cf| QueryCustomFieldColumn.new(cf) }
106 map {|cf| QueryCustomFieldColumn.new(cf) }
101 @available_columns += IssueCustomField.visible.
107 @available_columns += IssueCustomField.visible.
102 map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
108 map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
103 @available_columns
109 @available_columns
104 end
110 end
105
111
106 def default_columns_names
112 def default_columns_names
107 @default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours]
113 @default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours]
108 end
114 end
109
115
110 def results_scope(options={})
116 def results_scope(options={})
111 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
117 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
112
118
113 TimeEntry.visible.
119 TimeEntry.visible.
114 where(statement).
120 where(statement).
115 order(order_option).
121 order(order_option).
116 joins(joins_for_order_statement(order_option.join(','))).
122 joins(joins_for_order_statement(order_option.join(','))).
117 includes(:activity).
123 includes(:activity).
118 references(:activity)
124 references(:activity)
119 end
125 end
120
126
121 def sql_for_issue_id_field(field, operator, value)
127 def sql_for_issue_id_field(field, operator, value)
122 case operator
128 case operator
123 when "="
129 when "="
124 "#{TimeEntry.table_name}.issue_id = #{value.first.to_i}"
130 "#{TimeEntry.table_name}.issue_id = #{value.first.to_i}"
125 when "~"
131 when "~"
126 issue = Issue.where(:id => value.first.to_i).first
132 issue = Issue.where(:id => value.first.to_i).first
127 if issue && (issue_ids = issue.self_and_descendants.pluck(:id)).any?
133 if issue && (issue_ids = issue.self_and_descendants.pluck(:id)).any?
128 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
134 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
129 else
135 else
130 "1=0"
136 "1=0"
131 end
137 end
132 when "!*"
138 when "!*"
133 "#{TimeEntry.table_name}.issue_id IS NULL"
139 "#{TimeEntry.table_name}.issue_id IS NULL"
134 when "*"
140 when "*"
135 "#{TimeEntry.table_name}.issue_id IS NOT NULL"
141 "#{TimeEntry.table_name}.issue_id IS NOT NULL"
136 end
142 end
137 end
143 end
138
144
145 def sql_for_issue_fixed_version_id_field(field, operator, value)
146 issue_ids = Issue.where(:fixed_version_id => value.first.to_i).pluck(:id)
147 case operator
148 when "="
149 if issue_ids.any?
150 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
151 else
152 "1=0"
153 end
154 when "!"
155 if issue_ids.any?
156 "#{TimeEntry.table_name}.issue_id NOT IN (#{issue_ids.join(',')})"
157 else
158 "1=1"
159 end
160 end
161 end
162
139 def sql_for_activity_id_field(field, operator, value)
163 def sql_for_activity_id_field(field, operator, value)
140 condition_on_id = sql_for_field(field, operator, value, Enumeration.table_name, 'id')
164 condition_on_id = sql_for_field(field, operator, value, Enumeration.table_name, 'id')
141 condition_on_parent_id = sql_for_field(field, operator, value, Enumeration.table_name, 'parent_id')
165 condition_on_parent_id = sql_for_field(field, operator, value, Enumeration.table_name, 'parent_id')
142 ids = value.map(&:to_i).join(',')
166 ids = value.map(&:to_i).join(',')
143 table_name = Enumeration.table_name
167 table_name = Enumeration.table_name
144 if operator == '='
168 if operator == '='
145 "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))"
169 "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))"
146 else
170 else
147 "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))"
171 "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))"
148 end
172 end
149 end
173 end
150
174
151 # Accepts :from/:to params as shortcut filters
175 # Accepts :from/:to params as shortcut filters
152 def build_from_params(params)
176 def build_from_params(params)
153 super
177 super
154 if params[:from].present? && params[:to].present?
178 if params[:from].present? && params[:to].present?
155 add_filter('spent_on', '><', [params[:from], params[:to]])
179 add_filter('spent_on', '><', [params[:from], params[:to]])
156 elsif params[:from].present?
180 elsif params[:from].present?
157 add_filter('spent_on', '>=', [params[:from]])
181 add_filter('spent_on', '>=', [params[:from]])
158 elsif params[:to].present?
182 elsif params[:to].present?
159 add_filter('spent_on', '<=', [params[:to]])
183 add_filter('spent_on', '<=', [params[:to]])
160 end
184 end
161 self
185 self
162 end
186 end
163 end
187 end
@@ -1,823 +1,844
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # Redmine - project management software
2 # Redmine - project management software
3 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 #
4 #
5 # This program is free software; you can redistribute it and/or
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
8 # of the License, or (at your option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful,
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
13 # GNU General Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
18
19 require File.expand_path('../../test_helper', __FILE__)
19 require File.expand_path('../../test_helper', __FILE__)
20
20
21 class TimelogControllerTest < ActionController::TestCase
21 class TimelogControllerTest < ActionController::TestCase
22 fixtures :projects, :enabled_modules, :roles, :members,
22 fixtures :projects, :enabled_modules, :roles, :members,
23 :member_roles, :issues, :time_entries, :users,
23 :member_roles, :issues, :time_entries, :users,
24 :trackers, :enumerations, :issue_statuses,
24 :trackers, :enumerations, :issue_statuses,
25 :custom_fields, :custom_values,
25 :custom_fields, :custom_values,
26 :projects_trackers, :custom_fields_trackers,
26 :projects_trackers, :custom_fields_trackers,
27 :custom_fields_projects
27 :custom_fields_projects
28
28
29 include Redmine::I18n
29 include Redmine::I18n
30
30
31 def test_new
31 def test_new
32 @request.session[:user_id] = 3
32 @request.session[:user_id] = 3
33 get :new
33 get :new
34 assert_response :success
34 assert_response :success
35 assert_template 'new'
35 assert_template 'new'
36 assert_select 'input[name=?][type=hidden]', 'project_id', 0
36 assert_select 'input[name=?][type=hidden]', 'project_id', 0
37 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
37 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
38 assert_select 'select[name=?]', 'time_entry[project_id]' do
38 assert_select 'select[name=?]', 'time_entry[project_id]' do
39 # blank option for project
39 # blank option for project
40 assert_select 'option[value=""]'
40 assert_select 'option[value=""]'
41 end
41 end
42 end
42 end
43
43
44 def test_new_with_project_id
44 def test_new_with_project_id
45 @request.session[:user_id] = 3
45 @request.session[:user_id] = 3
46 get :new, :project_id => 1
46 get :new, :project_id => 1
47 assert_response :success
47 assert_response :success
48 assert_template 'new'
48 assert_template 'new'
49 assert_select 'input[name=?][type=hidden]', 'project_id'
49 assert_select 'input[name=?][type=hidden]', 'project_id'
50 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
50 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
51 assert_select 'select[name=?]', 'time_entry[project_id]', 0
51 assert_select 'select[name=?]', 'time_entry[project_id]', 0
52 end
52 end
53
53
54 def test_new_with_issue_id
54 def test_new_with_issue_id
55 @request.session[:user_id] = 3
55 @request.session[:user_id] = 3
56 get :new, :issue_id => 2
56 get :new, :issue_id => 2
57 assert_response :success
57 assert_response :success
58 assert_template 'new'
58 assert_template 'new'
59 assert_select 'input[name=?][type=hidden]', 'project_id', 0
59 assert_select 'input[name=?][type=hidden]', 'project_id', 0
60 assert_select 'input[name=?][type=hidden]', 'issue_id'
60 assert_select 'input[name=?][type=hidden]', 'issue_id'
61 assert_select 'select[name=?]', 'time_entry[project_id]', 0
61 assert_select 'select[name=?]', 'time_entry[project_id]', 0
62 end
62 end
63
63
64 def test_new_without_project_should_prefill_the_form
64 def test_new_without_project_should_prefill_the_form
65 @request.session[:user_id] = 3
65 @request.session[:user_id] = 3
66 get :new, :time_entry => {:project_id => '1'}
66 get :new, :time_entry => {:project_id => '1'}
67 assert_response :success
67 assert_response :success
68 assert_template 'new'
68 assert_template 'new'
69 assert_select 'select[name=?]', 'time_entry[project_id]' do
69 assert_select 'select[name=?]', 'time_entry[project_id]' do
70 assert_select 'option[value="1"][selected=selected]'
70 assert_select 'option[value="1"][selected=selected]'
71 end
71 end
72 end
72 end
73
73
74 def test_new_without_project_should_deny_without_permission
74 def test_new_without_project_should_deny_without_permission
75 Role.all.each {|role| role.remove_permission! :log_time}
75 Role.all.each {|role| role.remove_permission! :log_time}
76 @request.session[:user_id] = 3
76 @request.session[:user_id] = 3
77
77
78 get :new
78 get :new
79 assert_response 403
79 assert_response 403
80 end
80 end
81
81
82 def test_new_should_select_default_activity
82 def test_new_should_select_default_activity
83 @request.session[:user_id] = 3
83 @request.session[:user_id] = 3
84 get :new, :project_id => 1
84 get :new, :project_id => 1
85 assert_response :success
85 assert_response :success
86 assert_select 'select[name=?]', 'time_entry[activity_id]' do
86 assert_select 'select[name=?]', 'time_entry[activity_id]' do
87 assert_select 'option[selected=selected]', :text => 'Development'
87 assert_select 'option[selected=selected]', :text => 'Development'
88 end
88 end
89 end
89 end
90
90
91 def test_new_should_only_show_active_time_entry_activities
91 def test_new_should_only_show_active_time_entry_activities
92 @request.session[:user_id] = 3
92 @request.session[:user_id] = 3
93 get :new, :project_id => 1
93 get :new, :project_id => 1
94 assert_response :success
94 assert_response :success
95 assert_select 'option', :text => 'Inactive Activity', :count => 0
95 assert_select 'option', :text => 'Inactive Activity', :count => 0
96 end
96 end
97
97
98 def test_post_new_as_js_should_update_activity_options
98 def test_post_new_as_js_should_update_activity_options
99 @request.session[:user_id] = 3
99 @request.session[:user_id] = 3
100 post :new, :time_entry => {:project_id => 1}, :format => 'js'
100 post :new, :time_entry => {:project_id => 1}, :format => 'js'
101 assert_response :success
101 assert_response :success
102 assert_include '#time_entry_activity_id', response.body
102 assert_include '#time_entry_activity_id', response.body
103 end
103 end
104
104
105 def test_get_edit_existing_time
105 def test_get_edit_existing_time
106 @request.session[:user_id] = 2
106 @request.session[:user_id] = 2
107 get :edit, :id => 2, :project_id => nil
107 get :edit, :id => 2, :project_id => nil
108 assert_response :success
108 assert_response :success
109 assert_template 'edit'
109 assert_template 'edit'
110 assert_select 'form[action=?]', '/time_entries/2'
110 assert_select 'form[action=?]', '/time_entries/2'
111 end
111 end
112
112
113 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
113 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
114 te = TimeEntry.find(1)
114 te = TimeEntry.find(1)
115 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
115 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
116 te.save!(:validate => false)
116 te.save!(:validate => false)
117
117
118 @request.session[:user_id] = 1
118 @request.session[:user_id] = 1
119 get :edit, :project_id => 1, :id => 1
119 get :edit, :project_id => 1, :id => 1
120 assert_response :success
120 assert_response :success
121 assert_template 'edit'
121 assert_template 'edit'
122 # Blank option since nothing is pre-selected
122 # Blank option since nothing is pre-selected
123 assert_select 'option', :text => '--- Please select ---'
123 assert_select 'option', :text => '--- Please select ---'
124 end
124 end
125
125
126 def test_post_create
126 def test_post_create
127 @request.session[:user_id] = 3
127 @request.session[:user_id] = 3
128 assert_difference 'TimeEntry.count' do
128 assert_difference 'TimeEntry.count' do
129 post :create, :project_id => 1,
129 post :create, :project_id => 1,
130 :time_entry => {:comments => 'Some work on TimelogControllerTest',
130 :time_entry => {:comments => 'Some work on TimelogControllerTest',
131 # Not the default activity
131 # Not the default activity
132 :activity_id => '11',
132 :activity_id => '11',
133 :spent_on => '2008-03-14',
133 :spent_on => '2008-03-14',
134 :issue_id => '1',
134 :issue_id => '1',
135 :hours => '7.3'}
135 :hours => '7.3'}
136 assert_redirected_to '/projects/ecookbook/time_entries'
136 assert_redirected_to '/projects/ecookbook/time_entries'
137 end
137 end
138
138
139 t = TimeEntry.order('id DESC').first
139 t = TimeEntry.order('id DESC').first
140 assert_not_nil t
140 assert_not_nil t
141 assert_equal 'Some work on TimelogControllerTest', t.comments
141 assert_equal 'Some work on TimelogControllerTest', t.comments
142 assert_equal 1, t.project_id
142 assert_equal 1, t.project_id
143 assert_equal 1, t.issue_id
143 assert_equal 1, t.issue_id
144 assert_equal 11, t.activity_id
144 assert_equal 11, t.activity_id
145 assert_equal 7.3, t.hours
145 assert_equal 7.3, t.hours
146 assert_equal 3, t.user_id
146 assert_equal 3, t.user_id
147 end
147 end
148
148
149 def test_post_create_with_blank_issue
149 def test_post_create_with_blank_issue
150 @request.session[:user_id] = 3
150 @request.session[:user_id] = 3
151 assert_difference 'TimeEntry.count' do
151 assert_difference 'TimeEntry.count' do
152 post :create, :project_id => 1,
152 post :create, :project_id => 1,
153 :time_entry => {:comments => 'Some work on TimelogControllerTest',
153 :time_entry => {:comments => 'Some work on TimelogControllerTest',
154 # Not the default activity
154 # Not the default activity
155 :activity_id => '11',
155 :activity_id => '11',
156 :issue_id => '',
156 :issue_id => '',
157 :spent_on => '2008-03-14',
157 :spent_on => '2008-03-14',
158 :hours => '7.3'}
158 :hours => '7.3'}
159 assert_redirected_to '/projects/ecookbook/time_entries'
159 assert_redirected_to '/projects/ecookbook/time_entries'
160 end
160 end
161
161
162 t = TimeEntry.order('id DESC').first
162 t = TimeEntry.order('id DESC').first
163 assert_not_nil t
163 assert_not_nil t
164 assert_equal 'Some work on TimelogControllerTest', t.comments
164 assert_equal 'Some work on TimelogControllerTest', t.comments
165 assert_equal 1, t.project_id
165 assert_equal 1, t.project_id
166 assert_nil t.issue_id
166 assert_nil t.issue_id
167 assert_equal 11, t.activity_id
167 assert_equal 11, t.activity_id
168 assert_equal 7.3, t.hours
168 assert_equal 7.3, t.hours
169 assert_equal 3, t.user_id
169 assert_equal 3, t.user_id
170 end
170 end
171
171
172 def test_create_on_project_with_time_tracking_disabled_should_fail
172 def test_create_on_project_with_time_tracking_disabled_should_fail
173 Project.find(1).disable_module! :time_tracking
173 Project.find(1).disable_module! :time_tracking
174
174
175 @request.session[:user_id] = 2
175 @request.session[:user_id] = 2
176 assert_no_difference 'TimeEntry.count' do
176 assert_no_difference 'TimeEntry.count' do
177 post :create, :time_entry => {
177 post :create, :time_entry => {
178 :project_id => '1', :issue_id => '',
178 :project_id => '1', :issue_id => '',
179 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
179 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
180 }
180 }
181 end
181 end
182 end
182 end
183
183
184 def test_create_on_project_without_permission_should_fail
184 def test_create_on_project_without_permission_should_fail
185 Role.find(1).remove_permission! :log_time
185 Role.find(1).remove_permission! :log_time
186
186
187 @request.session[:user_id] = 2
187 @request.session[:user_id] = 2
188 assert_no_difference 'TimeEntry.count' do
188 assert_no_difference 'TimeEntry.count' do
189 post :create, :time_entry => {
189 post :create, :time_entry => {
190 :project_id => '1', :issue_id => '',
190 :project_id => '1', :issue_id => '',
191 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
191 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
192 }
192 }
193 end
193 end
194 end
194 end
195
195
196 def test_create_on_issue_in_project_with_time_tracking_disabled_should_fail
196 def test_create_on_issue_in_project_with_time_tracking_disabled_should_fail
197 Project.find(1).disable_module! :time_tracking
197 Project.find(1).disable_module! :time_tracking
198
198
199 @request.session[:user_id] = 2
199 @request.session[:user_id] = 2
200 assert_no_difference 'TimeEntry.count' do
200 assert_no_difference 'TimeEntry.count' do
201 post :create, :time_entry => {
201 post :create, :time_entry => {
202 :project_id => '', :issue_id => '1',
202 :project_id => '', :issue_id => '1',
203 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
203 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
204 }
204 }
205 assert_select_error /Issue is invalid/
205 assert_select_error /Issue is invalid/
206 end
206 end
207 end
207 end
208
208
209 def test_create_on_issue_in_project_without_permission_should_fail
209 def test_create_on_issue_in_project_without_permission_should_fail
210 Role.find(1).remove_permission! :log_time
210 Role.find(1).remove_permission! :log_time
211
211
212 @request.session[:user_id] = 2
212 @request.session[:user_id] = 2
213 assert_no_difference 'TimeEntry.count' do
213 assert_no_difference 'TimeEntry.count' do
214 post :create, :time_entry => {
214 post :create, :time_entry => {
215 :project_id => '', :issue_id => '1',
215 :project_id => '', :issue_id => '1',
216 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
216 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
217 }
217 }
218 assert_select_error /Issue is invalid/
218 assert_select_error /Issue is invalid/
219 end
219 end
220 end
220 end
221
221
222 def test_create_on_issue_that_is_not_visible_should_not_disclose_subject
222 def test_create_on_issue_that_is_not_visible_should_not_disclose_subject
223 issue = Issue.generate!(:subject => "issue_that_is_not_visible", :is_private => true)
223 issue = Issue.generate!(:subject => "issue_that_is_not_visible", :is_private => true)
224 assert !issue.visible?(User.find(3))
224 assert !issue.visible?(User.find(3))
225
225
226 @request.session[:user_id] = 3
226 @request.session[:user_id] = 3
227 assert_no_difference 'TimeEntry.count' do
227 assert_no_difference 'TimeEntry.count' do
228 post :create, :time_entry => {
228 post :create, :time_entry => {
229 :project_id => '', :issue_id => issue.id.to_s,
229 :project_id => '', :issue_id => issue.id.to_s,
230 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
230 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
231 }
231 }
232 end
232 end
233 assert_select_error /Issue is invalid/
233 assert_select_error /Issue is invalid/
234 assert_select "input[name=?][value=?]", "time_entry[issue_id]", issue.id.to_s
234 assert_select "input[name=?][value=?]", "time_entry[issue_id]", issue.id.to_s
235 assert_select "#time_entry_issue", 0
235 assert_select "#time_entry_issue", 0
236 assert !response.body.include?('issue_that_is_not_visible')
236 assert !response.body.include?('issue_that_is_not_visible')
237 end
237 end
238
238
239 def test_create_and_continue_at_project_level
239 def test_create_and_continue_at_project_level
240 @request.session[:user_id] = 2
240 @request.session[:user_id] = 2
241 assert_difference 'TimeEntry.count' do
241 assert_difference 'TimeEntry.count' do
242 post :create, :time_entry => {:project_id => '1',
242 post :create, :time_entry => {:project_id => '1',
243 :activity_id => '11',
243 :activity_id => '11',
244 :issue_id => '',
244 :issue_id => '',
245 :spent_on => '2008-03-14',
245 :spent_on => '2008-03-14',
246 :hours => '7.3'},
246 :hours => '7.3'},
247 :continue => '1'
247 :continue => '1'
248 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
248 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
249 end
249 end
250 end
250 end
251
251
252 def test_create_and_continue_at_issue_level
252 def test_create_and_continue_at_issue_level
253 @request.session[:user_id] = 2
253 @request.session[:user_id] = 2
254 assert_difference 'TimeEntry.count' do
254 assert_difference 'TimeEntry.count' do
255 post :create, :time_entry => {:project_id => '',
255 post :create, :time_entry => {:project_id => '',
256 :activity_id => '11',
256 :activity_id => '11',
257 :issue_id => '1',
257 :issue_id => '1',
258 :spent_on => '2008-03-14',
258 :spent_on => '2008-03-14',
259 :hours => '7.3'},
259 :hours => '7.3'},
260 :continue => '1'
260 :continue => '1'
261 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
261 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
262 end
262 end
263 end
263 end
264
264
265 def test_create_and_continue_with_project_id
265 def test_create_and_continue_with_project_id
266 @request.session[:user_id] = 2
266 @request.session[:user_id] = 2
267 assert_difference 'TimeEntry.count' do
267 assert_difference 'TimeEntry.count' do
268 post :create, :project_id => 1,
268 post :create, :project_id => 1,
269 :time_entry => {:activity_id => '11',
269 :time_entry => {:activity_id => '11',
270 :issue_id => '',
270 :issue_id => '',
271 :spent_on => '2008-03-14',
271 :spent_on => '2008-03-14',
272 :hours => '7.3'},
272 :hours => '7.3'},
273 :continue => '1'
273 :continue => '1'
274 assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D='
274 assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D='
275 end
275 end
276 end
276 end
277
277
278 def test_create_and_continue_with_issue_id
278 def test_create_and_continue_with_issue_id
279 @request.session[:user_id] = 2
279 @request.session[:user_id] = 2
280 assert_difference 'TimeEntry.count' do
280 assert_difference 'TimeEntry.count' do
281 post :create, :issue_id => 1,
281 post :create, :issue_id => 1,
282 :time_entry => {:activity_id => '11',
282 :time_entry => {:activity_id => '11',
283 :issue_id => '1',
283 :issue_id => '1',
284 :spent_on => '2008-03-14',
284 :spent_on => '2008-03-14',
285 :hours => '7.3'},
285 :hours => '7.3'},
286 :continue => '1'
286 :continue => '1'
287 assert_redirected_to '/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
287 assert_redirected_to '/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
288 end
288 end
289 end
289 end
290
290
291 def test_create_without_log_time_permission_should_be_denied
291 def test_create_without_log_time_permission_should_be_denied
292 @request.session[:user_id] = 2
292 @request.session[:user_id] = 2
293 Role.find_by_name('Manager').remove_permission! :log_time
293 Role.find_by_name('Manager').remove_permission! :log_time
294 post :create, :project_id => 1,
294 post :create, :project_id => 1,
295 :time_entry => {:activity_id => '11',
295 :time_entry => {:activity_id => '11',
296 :issue_id => '',
296 :issue_id => '',
297 :spent_on => '2008-03-14',
297 :spent_on => '2008-03-14',
298 :hours => '7.3'}
298 :hours => '7.3'}
299
299
300 assert_response 403
300 assert_response 403
301 end
301 end
302
302
303 def test_create_without_project_and_issue_should_fail
303 def test_create_without_project_and_issue_should_fail
304 @request.session[:user_id] = 2
304 @request.session[:user_id] = 2
305 post :create, :time_entry => {:issue_id => ''}
305 post :create, :time_entry => {:issue_id => ''}
306
306
307 assert_response :success
307 assert_response :success
308 assert_template 'new'
308 assert_template 'new'
309 end
309 end
310
310
311 def test_create_with_failure
311 def test_create_with_failure
312 @request.session[:user_id] = 2
312 @request.session[:user_id] = 2
313 post :create, :project_id => 1,
313 post :create, :project_id => 1,
314 :time_entry => {:activity_id => '',
314 :time_entry => {:activity_id => '',
315 :issue_id => '',
315 :issue_id => '',
316 :spent_on => '2008-03-14',
316 :spent_on => '2008-03-14',
317 :hours => '7.3'}
317 :hours => '7.3'}
318
318
319 assert_response :success
319 assert_response :success
320 assert_template 'new'
320 assert_template 'new'
321 end
321 end
322
322
323 def test_create_without_project
323 def test_create_without_project
324 @request.session[:user_id] = 2
324 @request.session[:user_id] = 2
325 assert_difference 'TimeEntry.count' do
325 assert_difference 'TimeEntry.count' do
326 post :create, :time_entry => {:project_id => '1',
326 post :create, :time_entry => {:project_id => '1',
327 :activity_id => '11',
327 :activity_id => '11',
328 :issue_id => '',
328 :issue_id => '',
329 :spent_on => '2008-03-14',
329 :spent_on => '2008-03-14',
330 :hours => '7.3'}
330 :hours => '7.3'}
331 end
331 end
332
332
333 assert_redirected_to '/projects/ecookbook/time_entries'
333 assert_redirected_to '/projects/ecookbook/time_entries'
334 time_entry = TimeEntry.order('id DESC').first
334 time_entry = TimeEntry.order('id DESC').first
335 assert_equal 1, time_entry.project_id
335 assert_equal 1, time_entry.project_id
336 end
336 end
337
337
338 def test_create_without_project_should_fail_with_issue_not_inside_project
338 def test_create_without_project_should_fail_with_issue_not_inside_project
339 @request.session[:user_id] = 2
339 @request.session[:user_id] = 2
340 assert_no_difference 'TimeEntry.count' do
340 assert_no_difference 'TimeEntry.count' do
341 post :create, :time_entry => {:project_id => '1',
341 post :create, :time_entry => {:project_id => '1',
342 :activity_id => '11',
342 :activity_id => '11',
343 :issue_id => '5',
343 :issue_id => '5',
344 :spent_on => '2008-03-14',
344 :spent_on => '2008-03-14',
345 :hours => '7.3'}
345 :hours => '7.3'}
346 end
346 end
347
347
348 assert_response :success
348 assert_response :success
349 assert assigns(:time_entry).errors[:issue_id].present?
349 assert assigns(:time_entry).errors[:issue_id].present?
350 end
350 end
351
351
352 def test_create_without_project_should_deny_without_permission
352 def test_create_without_project_should_deny_without_permission
353 @request.session[:user_id] = 2
353 @request.session[:user_id] = 2
354 Project.find(3).disable_module!(:time_tracking)
354 Project.find(3).disable_module!(:time_tracking)
355
355
356 assert_no_difference 'TimeEntry.count' do
356 assert_no_difference 'TimeEntry.count' do
357 post :create, :time_entry => {:project_id => '3',
357 post :create, :time_entry => {:project_id => '3',
358 :activity_id => '11',
358 :activity_id => '11',
359 :issue_id => '',
359 :issue_id => '',
360 :spent_on => '2008-03-14',
360 :spent_on => '2008-03-14',
361 :hours => '7.3'}
361 :hours => '7.3'}
362 end
362 end
363
363
364 assert_response 403
364 assert_response 403
365 end
365 end
366
366
367 def test_create_without_project_with_failure
367 def test_create_without_project_with_failure
368 @request.session[:user_id] = 2
368 @request.session[:user_id] = 2
369 assert_no_difference 'TimeEntry.count' do
369 assert_no_difference 'TimeEntry.count' do
370 post :create, :time_entry => {:project_id => '1',
370 post :create, :time_entry => {:project_id => '1',
371 :activity_id => '11',
371 :activity_id => '11',
372 :issue_id => '',
372 :issue_id => '',
373 :spent_on => '2008-03-14',
373 :spent_on => '2008-03-14',
374 :hours => ''}
374 :hours => ''}
375 end
375 end
376
376
377 assert_response :success
377 assert_response :success
378 assert_select 'select[name=?]', 'time_entry[project_id]' do
378 assert_select 'select[name=?]', 'time_entry[project_id]' do
379 assert_select 'option[value="1"][selected=selected]'
379 assert_select 'option[value="1"][selected=selected]'
380 end
380 end
381 end
381 end
382
382
383 def test_update
383 def test_update
384 entry = TimeEntry.find(1)
384 entry = TimeEntry.find(1)
385 assert_equal 1, entry.issue_id
385 assert_equal 1, entry.issue_id
386 assert_equal 2, entry.user_id
386 assert_equal 2, entry.user_id
387
387
388 @request.session[:user_id] = 1
388 @request.session[:user_id] = 1
389 put :update, :id => 1,
389 put :update, :id => 1,
390 :time_entry => {:issue_id => '2',
390 :time_entry => {:issue_id => '2',
391 :hours => '8'}
391 :hours => '8'}
392 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
392 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
393 entry.reload
393 entry.reload
394
394
395 assert_equal 8, entry.hours
395 assert_equal 8, entry.hours
396 assert_equal 2, entry.issue_id
396 assert_equal 2, entry.issue_id
397 assert_equal 2, entry.user_id
397 assert_equal 2, entry.user_id
398 end
398 end
399
399
400 def test_update_should_allow_to_change_issue_to_another_project
400 def test_update_should_allow_to_change_issue_to_another_project
401 entry = TimeEntry.generate!(:issue_id => 1)
401 entry = TimeEntry.generate!(:issue_id => 1)
402
402
403 @request.session[:user_id] = 1
403 @request.session[:user_id] = 1
404 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
404 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
405 assert_response 302
405 assert_response 302
406 entry.reload
406 entry.reload
407
407
408 assert_equal 5, entry.issue_id
408 assert_equal 5, entry.issue_id
409 assert_equal 3, entry.project_id
409 assert_equal 3, entry.project_id
410 end
410 end
411
411
412 def test_update_should_not_allow_to_change_issue_to_an_invalid_project
412 def test_update_should_not_allow_to_change_issue_to_an_invalid_project
413 entry = TimeEntry.generate!(:issue_id => 1)
413 entry = TimeEntry.generate!(:issue_id => 1)
414 Project.find(3).disable_module!(:time_tracking)
414 Project.find(3).disable_module!(:time_tracking)
415
415
416 @request.session[:user_id] = 1
416 @request.session[:user_id] = 1
417 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
417 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
418 assert_response 200
418 assert_response 200
419 assert_include "Issue is invalid", assigns(:time_entry).errors.full_messages
419 assert_include "Issue is invalid", assigns(:time_entry).errors.full_messages
420 end
420 end
421
421
422 def test_get_bulk_edit
422 def test_get_bulk_edit
423 @request.session[:user_id] = 2
423 @request.session[:user_id] = 2
424 get :bulk_edit, :ids => [1, 2]
424 get :bulk_edit, :ids => [1, 2]
425 assert_response :success
425 assert_response :success
426 assert_template 'bulk_edit'
426 assert_template 'bulk_edit'
427
427
428 assert_select 'ul#bulk-selection' do
428 assert_select 'ul#bulk-selection' do
429 assert_select 'li', 2
429 assert_select 'li', 2
430 assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours'
430 assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours'
431 end
431 end
432
432
433 assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do
433 assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do
434 # System wide custom field
434 # System wide custom field
435 assert_select 'select[name=?]', 'time_entry[custom_field_values][10]'
435 assert_select 'select[name=?]', 'time_entry[custom_field_values][10]'
436
436
437 # Activities
437 # Activities
438 assert_select 'select[name=?]', 'time_entry[activity_id]' do
438 assert_select 'select[name=?]', 'time_entry[activity_id]' do
439 assert_select 'option[value=""]', :text => '(No change)'
439 assert_select 'option[value=""]', :text => '(No change)'
440 assert_select 'option[value="9"]', :text => 'Design'
440 assert_select 'option[value="9"]', :text => 'Design'
441 end
441 end
442 end
442 end
443 end
443 end
444
444
445 def test_get_bulk_edit_on_different_projects
445 def test_get_bulk_edit_on_different_projects
446 @request.session[:user_id] = 2
446 @request.session[:user_id] = 2
447 get :bulk_edit, :ids => [1, 2, 6]
447 get :bulk_edit, :ids => [1, 2, 6]
448 assert_response :success
448 assert_response :success
449 assert_template 'bulk_edit'
449 assert_template 'bulk_edit'
450 end
450 end
451
451
452 def test_bulk_edit_with_edit_own_time_entries_permission
452 def test_bulk_edit_with_edit_own_time_entries_permission
453 @request.session[:user_id] = 2
453 @request.session[:user_id] = 2
454 Role.find_by_name('Manager').remove_permission! :edit_time_entries
454 Role.find_by_name('Manager').remove_permission! :edit_time_entries
455 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
455 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
456 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
456 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
457
457
458 get :bulk_edit, :ids => ids
458 get :bulk_edit, :ids => ids
459 assert_response :success
459 assert_response :success
460 end
460 end
461
461
462 def test_bulk_update
462 def test_bulk_update
463 @request.session[:user_id] = 2
463 @request.session[:user_id] = 2
464 # update time entry activity
464 # update time entry activity
465 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
465 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
466
466
467 assert_response 302
467 assert_response 302
468 # check that the issues were updated
468 # check that the issues were updated
469 assert_equal [9, 9], TimeEntry.where(:id => [1, 2]).collect {|i| i.activity_id}
469 assert_equal [9, 9], TimeEntry.where(:id => [1, 2]).collect {|i| i.activity_id}
470 end
470 end
471
471
472 def test_bulk_update_with_failure
472 def test_bulk_update_with_failure
473 @request.session[:user_id] = 2
473 @request.session[:user_id] = 2
474 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
474 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
475
475
476 assert_response 302
476 assert_response 302
477 assert_match /Failed to save 2 time entrie/, flash[:error]
477 assert_match /Failed to save 2 time entrie/, flash[:error]
478 end
478 end
479
479
480 def test_bulk_update_on_different_projects
480 def test_bulk_update_on_different_projects
481 @request.session[:user_id] = 2
481 @request.session[:user_id] = 2
482 # makes user a manager on the other project
482 # makes user a manager on the other project
483 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
483 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
484
484
485 # update time entry activity
485 # update time entry activity
486 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
486 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
487
487
488 assert_response 302
488 assert_response 302
489 # check that the issues were updated
489 # check that the issues were updated
490 assert_equal [9, 9, 9], TimeEntry.where(:id => [1, 2, 4]).collect {|i| i.activity_id}
490 assert_equal [9, 9, 9], TimeEntry.where(:id => [1, 2, 4]).collect {|i| i.activity_id}
491 end
491 end
492
492
493 def test_bulk_update_on_different_projects_without_rights
493 def test_bulk_update_on_different_projects_without_rights
494 @request.session[:user_id] = 3
494 @request.session[:user_id] = 3
495 user = User.find(3)
495 user = User.find(3)
496 action = { :controller => "timelog", :action => "bulk_update" }
496 action = { :controller => "timelog", :action => "bulk_update" }
497 assert user.allowed_to?(action, TimeEntry.find(1).project)
497 assert user.allowed_to?(action, TimeEntry.find(1).project)
498 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
498 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
499 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
499 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
500 assert_response 403
500 assert_response 403
501 end
501 end
502
502
503 def test_bulk_update_with_edit_own_time_entries_permission
503 def test_bulk_update_with_edit_own_time_entries_permission
504 @request.session[:user_id] = 2
504 @request.session[:user_id] = 2
505 Role.find_by_name('Manager').remove_permission! :edit_time_entries
505 Role.find_by_name('Manager').remove_permission! :edit_time_entries
506 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
506 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
507 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
507 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
508
508
509 post :bulk_update, :ids => ids, :time_entry => { :activity_id => 9 }
509 post :bulk_update, :ids => ids, :time_entry => { :activity_id => 9 }
510 assert_response 302
510 assert_response 302
511 end
511 end
512
512
513 def test_bulk_update_with_edit_own_time_entries_permissions_should_be_denied_for_time_entries_of_other_user
513 def test_bulk_update_with_edit_own_time_entries_permissions_should_be_denied_for_time_entries_of_other_user
514 @request.session[:user_id] = 2
514 @request.session[:user_id] = 2
515 Role.find_by_name('Manager').remove_permission! :edit_time_entries
515 Role.find_by_name('Manager').remove_permission! :edit_time_entries
516 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
516 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
517
517
518 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9 }
518 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9 }
519 assert_response 403
519 assert_response 403
520 end
520 end
521
521
522 def test_bulk_update_custom_field
522 def test_bulk_update_custom_field
523 @request.session[:user_id] = 2
523 @request.session[:user_id] = 2
524 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
524 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
525
525
526 assert_response 302
526 assert_response 302
527 assert_equal ["0", "0"], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(10).value}
527 assert_equal ["0", "0"], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(10).value}
528 end
528 end
529
529
530 def test_bulk_update_clear_custom_field
530 def test_bulk_update_clear_custom_field
531 field = TimeEntryCustomField.generate!(:field_format => 'string')
531 field = TimeEntryCustomField.generate!(:field_format => 'string')
532 @request.session[:user_id] = 2
532 @request.session[:user_id] = 2
533 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {field.id.to_s => '__none__'} }
533 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {field.id.to_s => '__none__'} }
534
534
535 assert_response 302
535 assert_response 302
536 assert_equal ["", ""], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(field).value}
536 assert_equal ["", ""], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(field).value}
537 end
537 end
538
538
539 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
539 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
540 @request.session[:user_id] = 2
540 @request.session[:user_id] = 2
541 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
541 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
542
542
543 assert_response :redirect
543 assert_response :redirect
544 assert_redirected_to '/time_entries'
544 assert_redirected_to '/time_entries'
545 end
545 end
546
546
547 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
547 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
548 @request.session[:user_id] = 2
548 @request.session[:user_id] = 2
549 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
549 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
550
550
551 assert_response :redirect
551 assert_response :redirect
552 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
552 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
553 end
553 end
554
554
555 def test_post_bulk_update_without_edit_permission_should_be_denied
555 def test_post_bulk_update_without_edit_permission_should_be_denied
556 @request.session[:user_id] = 2
556 @request.session[:user_id] = 2
557 Role.find_by_name('Manager').remove_permission! :edit_time_entries
557 Role.find_by_name('Manager').remove_permission! :edit_time_entries
558 post :bulk_update, :ids => [1,2]
558 post :bulk_update, :ids => [1,2]
559
559
560 assert_response 403
560 assert_response 403
561 end
561 end
562
562
563 def test_destroy
563 def test_destroy
564 @request.session[:user_id] = 2
564 @request.session[:user_id] = 2
565 delete :destroy, :id => 1
565 delete :destroy, :id => 1
566 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
566 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
567 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
567 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
568 assert_nil TimeEntry.find_by_id(1)
568 assert_nil TimeEntry.find_by_id(1)
569 end
569 end
570
570
571 def test_destroy_should_fail
571 def test_destroy_should_fail
572 # simulate that this fails (e.g. due to a plugin), see #5700
572 # simulate that this fails (e.g. due to a plugin), see #5700
573 TimeEntry.any_instance.expects(:destroy).returns(false)
573 TimeEntry.any_instance.expects(:destroy).returns(false)
574
574
575 @request.session[:user_id] = 2
575 @request.session[:user_id] = 2
576 delete :destroy, :id => 1
576 delete :destroy, :id => 1
577 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
577 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
578 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
578 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
579 assert_not_nil TimeEntry.find_by_id(1)
579 assert_not_nil TimeEntry.find_by_id(1)
580 end
580 end
581
581
582 def test_index_all_projects
582 def test_index_all_projects
583 get :index
583 get :index
584 assert_response :success
584 assert_response :success
585 assert_template 'index'
585 assert_template 'index'
586 assert_not_nil assigns(:total_hours)
586 assert_not_nil assigns(:total_hours)
587 assert_equal "162.90", "%.2f" % assigns(:total_hours)
587 assert_equal "162.90", "%.2f" % assigns(:total_hours)
588 assert_select 'form#query_form[action=?]', '/time_entries'
588 assert_select 'form#query_form[action=?]', '/time_entries'
589 end
589 end
590
590
591 def test_index_all_projects_should_show_log_time_link
591 def test_index_all_projects_should_show_log_time_link
592 @request.session[:user_id] = 2
592 @request.session[:user_id] = 2
593 get :index
593 get :index
594 assert_response :success
594 assert_response :success
595 assert_template 'index'
595 assert_template 'index'
596 assert_select 'a[href=?]', '/time_entries/new', :text => /Log time/
596 assert_select 'a[href=?]', '/time_entries/new', :text => /Log time/
597 end
597 end
598
598
599 def test_index_my_spent_time
599 def test_index_my_spent_time
600 @request.session[:user_id] = 2
600 @request.session[:user_id] = 2
601 get :index, :user_id => 'me'
601 get :index, :user_id => 'me'
602 assert_response :success
602 assert_response :success
603 assert_template 'index'
603 assert_template 'index'
604 assert assigns(:entries).all? {|entry| entry.user_id == 2}
604 assert assigns(:entries).all? {|entry| entry.user_id == 2}
605 end
605 end
606
606
607 def test_index_at_project_level
607 def test_index_at_project_level
608 get :index, :project_id => 'ecookbook'
608 get :index, :project_id => 'ecookbook'
609 assert_response :success
609 assert_response :success
610 assert_template 'index'
610 assert_template 'index'
611 assert_not_nil assigns(:entries)
611 assert_not_nil assigns(:entries)
612 assert_equal 4, assigns(:entries).size
612 assert_equal 4, assigns(:entries).size
613 # project and subproject
613 # project and subproject
614 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
614 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
615 assert_not_nil assigns(:total_hours)
615 assert_not_nil assigns(:total_hours)
616 assert_equal "162.90", "%.2f" % assigns(:total_hours)
616 assert_equal "162.90", "%.2f" % assigns(:total_hours)
617 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
617 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
618 end
618 end
619
619
620 def test_index_with_display_subprojects_issues_to_false_should_not_include_subproject_entries
620 def test_index_with_display_subprojects_issues_to_false_should_not_include_subproject_entries
621 entry = TimeEntry.generate!(:project => Project.find(3))
621 entry = TimeEntry.generate!(:project => Project.find(3))
622
622
623 with_settings :display_subprojects_issues => '0' do
623 with_settings :display_subprojects_issues => '0' do
624 get :index, :project_id => 'ecookbook'
624 get :index, :project_id => 'ecookbook'
625 assert_response :success
625 assert_response :success
626 assert_template 'index'
626 assert_template 'index'
627 assert_not_include entry, assigns(:entries)
627 assert_not_include entry, assigns(:entries)
628 end
628 end
629 end
629 end
630
630
631 def test_index_with_display_subprojects_issues_to_false_and_subproject_filter_should_include_subproject_entries
631 def test_index_with_display_subprojects_issues_to_false_and_subproject_filter_should_include_subproject_entries
632 entry = TimeEntry.generate!(:project => Project.find(3))
632 entry = TimeEntry.generate!(:project => Project.find(3))
633
633
634 with_settings :display_subprojects_issues => '0' do
634 with_settings :display_subprojects_issues => '0' do
635 get :index, :project_id => 'ecookbook', :subproject_id => 3
635 get :index, :project_id => 'ecookbook', :subproject_id => 3
636 assert_response :success
636 assert_response :success
637 assert_template 'index'
637 assert_template 'index'
638 assert_include entry, assigns(:entries)
638 assert_include entry, assigns(:entries)
639 end
639 end
640 end
640 end
641
641
642 def test_index_at_project_level_with_issue_id_short_filter
643 issue = Issue.generate!(:project_id => 1)
644 TimeEntry.generate!(:issue => issue, :hours => 4)
645 TimeEntry.generate!(:issue => issue, :hours => 3)
646 @request.session[:user_id] = 2
647
648 get :index, :project_id => 'ecookbook', :issue_id => issue.id.to_s, :set_filter => 1
649 assert_select '.total-hours', :text => 'Total time: 7.00 hours'
650 end
651
652 def test_index_at_project_level_with_issue_fixed_version_id_short_filter
653 version = Version.generate!(:project_id => 1)
654 issue = Issue.generate!(:project_id => 1, :fixed_version => version)
655 TimeEntry.generate!(:issue => issue, :hours => 2)
656 TimeEntry.generate!(:issue => issue, :hours => 3)
657 @request.session[:user_id] = 2
658
659 get :index, :project_id => 'ecookbook', :"issue.fixed_version_id" => version.id.to_s, :set_filter => 1
660 assert_select '.total-hours', :text => 'Total time: 5.00 hours'
661 end
662
642 def test_index_at_project_level_with_date_range
663 def test_index_at_project_level_with_date_range
643 get :index, :project_id => 'ecookbook',
664 get :index, :project_id => 'ecookbook',
644 :f => ['spent_on'],
665 :f => ['spent_on'],
645 :op => {'spent_on' => '><'},
666 :op => {'spent_on' => '><'},
646 :v => {'spent_on' => ['2007-03-20', '2007-04-30']}
667 :v => {'spent_on' => ['2007-03-20', '2007-04-30']}
647 assert_response :success
668 assert_response :success
648 assert_template 'index'
669 assert_template 'index'
649 assert_not_nil assigns(:entries)
670 assert_not_nil assigns(:entries)
650 assert_equal 3, assigns(:entries).size
671 assert_equal 3, assigns(:entries).size
651 assert_not_nil assigns(:total_hours)
672 assert_not_nil assigns(:total_hours)
652 assert_equal "12.90", "%.2f" % assigns(:total_hours)
673 assert_equal "12.90", "%.2f" % assigns(:total_hours)
653 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
674 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
654 end
675 end
655
676
656 def test_index_at_project_level_with_date_range_using_from_and_to_params
677 def test_index_at_project_level_with_date_range_using_from_and_to_params
657 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
678 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
658 assert_response :success
679 assert_response :success
659 assert_template 'index'
680 assert_template 'index'
660 assert_not_nil assigns(:entries)
681 assert_not_nil assigns(:entries)
661 assert_equal 3, assigns(:entries).size
682 assert_equal 3, assigns(:entries).size
662 assert_not_nil assigns(:total_hours)
683 assert_not_nil assigns(:total_hours)
663 assert_equal "12.90", "%.2f" % assigns(:total_hours)
684 assert_equal "12.90", "%.2f" % assigns(:total_hours)
664 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
685 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
665 end
686 end
666
687
667 def test_index_at_project_level_with_period
688 def test_index_at_project_level_with_period
668 get :index, :project_id => 'ecookbook',
689 get :index, :project_id => 'ecookbook',
669 :f => ['spent_on'],
690 :f => ['spent_on'],
670 :op => {'spent_on' => '>t-'},
691 :op => {'spent_on' => '>t-'},
671 :v => {'spent_on' => ['7']}
692 :v => {'spent_on' => ['7']}
672 assert_response :success
693 assert_response :success
673 assert_template 'index'
694 assert_template 'index'
674 assert_not_nil assigns(:entries)
695 assert_not_nil assigns(:entries)
675 assert_not_nil assigns(:total_hours)
696 assert_not_nil assigns(:total_hours)
676 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
697 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
677 end
698 end
678
699
679 def test_index_should_sort_by_spent_on_and_created_on
700 def test_index_should_sort_by_spent_on_and_created_on
680 t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
701 t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
681 t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
702 t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
682 t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
703 t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
683
704
684 get :index, :project_id => 1,
705 get :index, :project_id => 1,
685 :f => ['spent_on'],
706 :f => ['spent_on'],
686 :op => {'spent_on' => '><'},
707 :op => {'spent_on' => '><'},
687 :v => {'spent_on' => ['2012-06-15', '2012-06-16']}
708 :v => {'spent_on' => ['2012-06-15', '2012-06-16']}
688 assert_response :success
709 assert_response :success
689 assert_equal [t2, t1, t3], assigns(:entries)
710 assert_equal [t2, t1, t3], assigns(:entries)
690
711
691 get :index, :project_id => 1,
712 get :index, :project_id => 1,
692 :f => ['spent_on'],
713 :f => ['spent_on'],
693 :op => {'spent_on' => '><'},
714 :op => {'spent_on' => '><'},
694 :v => {'spent_on' => ['2012-06-15', '2012-06-16']},
715 :v => {'spent_on' => ['2012-06-15', '2012-06-16']},
695 :sort => 'spent_on'
716 :sort => 'spent_on'
696 assert_response :success
717 assert_response :success
697 assert_equal [t3, t1, t2], assigns(:entries)
718 assert_equal [t3, t1, t2], assigns(:entries)
698 end
719 end
699
720
700 def test_index_with_filter_on_issue_custom_field
721 def test_index_with_filter_on_issue_custom_field
701 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
722 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
702 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
723 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
703
724
704 get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']}
725 get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']}
705 assert_response :success
726 assert_response :success
706 assert_equal [entry], assigns(:entries)
727 assert_equal [entry], assigns(:entries)
707 end
728 end
708
729
709 def test_index_with_issue_custom_field_column
730 def test_index_with_issue_custom_field_column
710 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
731 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
711 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
732 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
712
733
713 get :index, :c => %w(project spent_on issue comments hours issue.cf_2)
734 get :index, :c => %w(project spent_on issue comments hours issue.cf_2)
714 assert_response :success
735 assert_response :success
715 assert_include :'issue.cf_2', assigns(:query).column_names
736 assert_include :'issue.cf_2', assigns(:query).column_names
716 assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field'
737 assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field'
717 end
738 end
718
739
719 def test_index_with_time_entry_custom_field_column
740 def test_index_with_time_entry_custom_field_column
720 field = TimeEntryCustomField.generate!(:field_format => 'string')
741 field = TimeEntryCustomField.generate!(:field_format => 'string')
721 entry = TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value'})
742 entry = TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value'})
722 field_name = "cf_#{field.id}"
743 field_name = "cf_#{field.id}"
723
744
724 get :index, :c => ["hours", field_name]
745 get :index, :c => ["hours", field_name]
725 assert_response :success
746 assert_response :success
726 assert_include field_name.to_sym, assigns(:query).column_names
747 assert_include field_name.to_sym, assigns(:query).column_names
727 assert_select "td.#{field_name}", :text => 'CF Value'
748 assert_select "td.#{field_name}", :text => 'CF Value'
728 end
749 end
729
750
730 def test_index_with_time_entry_custom_field_sorting
751 def test_index_with_time_entry_custom_field_sorting
731 field = TimeEntryCustomField.generate!(:field_format => 'string', :name => 'String Field')
752 field = TimeEntryCustomField.generate!(:field_format => 'string', :name => 'String Field')
732 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 1'})
753 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 1'})
733 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 3'})
754 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 3'})
734 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 2'})
755 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 2'})
735 field_name = "cf_#{field.id}"
756 field_name = "cf_#{field.id}"
736
757
737 get :index, :c => ["hours", field_name], :sort => field_name
758 get :index, :c => ["hours", field_name], :sort => field_name
738 assert_response :success
759 assert_response :success
739 assert_include field_name.to_sym, assigns(:query).column_names
760 assert_include field_name.to_sym, assigns(:query).column_names
740 assert_select "th a.sort", :text => 'String Field'
761 assert_select "th a.sort", :text => 'String Field'
741
762
742 # Make sure that values are properly sorted
763 # Make sure that values are properly sorted
743 values = assigns(:entries).map {|e| e.custom_field_value(field)}.compact
764 values = assigns(:entries).map {|e| e.custom_field_value(field)}.compact
744 assert_equal 3, values.size
765 assert_equal 3, values.size
745 assert_equal values.sort, values
766 assert_equal values.sort, values
746 end
767 end
747
768
748 def test_index_with_query
769 def test_index_with_query
749 query = TimeEntryQuery.new(:project_id => 1, :name => 'Time Entry Query', :visibility => 2)
770 query = TimeEntryQuery.new(:project_id => 1, :name => 'Time Entry Query', :visibility => 2)
750 query.save!
771 query.save!
751 @request.session[:user_id] = 2
772 @request.session[:user_id] = 2
752
773
753 get :index, :project_id => 'ecookbook', :query_id => query.id
774 get :index, :project_id => 'ecookbook', :query_id => query.id
754 assert_response :success
775 assert_response :success
755 assert_select 'h2', :text => query.name
776 assert_select 'h2', :text => query.name
756 assert_select '#sidebar a.selected', :text => query.name
777 assert_select '#sidebar a.selected', :text => query.name
757 end
778 end
758
779
759 def test_index_atom_feed
780 def test_index_atom_feed
760 get :index, :project_id => 1, :format => 'atom'
781 get :index, :project_id => 1, :format => 'atom'
761 assert_response :success
782 assert_response :success
762 assert_equal 'application/atom+xml', @response.content_type
783 assert_equal 'application/atom+xml', @response.content_type
763 assert_not_nil assigns(:items)
784 assert_not_nil assigns(:items)
764 assert assigns(:items).first.is_a?(TimeEntry)
785 assert assigns(:items).first.is_a?(TimeEntry)
765 end
786 end
766
787
767 def test_index_at_project_level_should_include_csv_export_dialog
788 def test_index_at_project_level_should_include_csv_export_dialog
768 get :index, :project_id => 'ecookbook',
789 get :index, :project_id => 'ecookbook',
769 :f => ['spent_on'],
790 :f => ['spent_on'],
770 :op => {'spent_on' => '>='},
791 :op => {'spent_on' => '>='},
771 :v => {'spent_on' => ['2007-04-01']},
792 :v => {'spent_on' => ['2007-04-01']},
772 :c => ['spent_on', 'user']
793 :c => ['spent_on', 'user']
773 assert_response :success
794 assert_response :success
774
795
775 assert_select '#csv-export-options' do
796 assert_select '#csv-export-options' do
776 assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do
797 assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do
777 # filter
798 # filter
778 assert_select 'input[name=?][value=?]', 'f[]', 'spent_on'
799 assert_select 'input[name=?][value=?]', 'f[]', 'spent_on'
779 assert_select 'input[name=?][value=?]', 'op[spent_on]', '>='
800 assert_select 'input[name=?][value=?]', 'op[spent_on]', '>='
780 assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01'
801 assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01'
781 # columns
802 # columns
782 assert_select 'input[name=?][value=?]', 'c[]', 'spent_on'
803 assert_select 'input[name=?][value=?]', 'c[]', 'spent_on'
783 assert_select 'input[name=?][value=?]', 'c[]', 'user'
804 assert_select 'input[name=?][value=?]', 'c[]', 'user'
784 assert_select 'input[name=?]', 'c[]', 2
805 assert_select 'input[name=?]', 'c[]', 2
785 end
806 end
786 end
807 end
787 end
808 end
788
809
789 def test_index_cross_project_should_include_csv_export_dialog
810 def test_index_cross_project_should_include_csv_export_dialog
790 get :index
811 get :index
791 assert_response :success
812 assert_response :success
792
813
793 assert_select '#csv-export-options' do
814 assert_select '#csv-export-options' do
794 assert_select 'form[action=?][method=get]', '/time_entries.csv'
815 assert_select 'form[action=?][method=get]', '/time_entries.csv'
795 end
816 end
796 end
817 end
797
818
798 def test_index_csv_all_projects
819 def test_index_csv_all_projects
799 with_settings :date_format => '%m/%d/%Y' do
820 with_settings :date_format => '%m/%d/%Y' do
800 get :index, :format => 'csv'
821 get :index, :format => 'csv'
801 assert_response :success
822 assert_response :success
802 assert_equal 'text/csv; header=present', response.content_type
823 assert_equal 'text/csv; header=present', response.content_type
803 end
824 end
804 end
825 end
805
826
806 def test_index_csv
827 def test_index_csv
807 with_settings :date_format => '%m/%d/%Y' do
828 with_settings :date_format => '%m/%d/%Y' do
808 get :index, :project_id => 1, :format => 'csv'
829 get :index, :project_id => 1, :format => 'csv'
809 assert_response :success
830 assert_response :success
810 assert_equal 'text/csv; header=present', response.content_type
831 assert_equal 'text/csv; header=present', response.content_type
811 end
832 end
812 end
833 end
813
834
814 def test_index_csv_should_fill_issue_column_with_tracker_id_and_subject
835 def test_index_csv_should_fill_issue_column_with_tracker_id_and_subject
815 issue = Issue.find(1)
836 issue = Issue.find(1)
816 entry = TimeEntry.generate!(:issue => issue, :comments => "Issue column content test")
837 entry = TimeEntry.generate!(:issue => issue, :comments => "Issue column content test")
817
838
818 get :index, :format => 'csv'
839 get :index, :format => 'csv'
819 line = response.body.split("\n").detect {|l| l.include?(entry.comments)}
840 line = response.body.split("\n").detect {|l| l.include?(entry.comments)}
820 assert_not_nil line
841 assert_not_nil line
821 assert_include "#{issue.tracker} #1: #{issue.subject}", line
842 assert_include "#{issue.tracker} #1: #{issue.subject}", line
822 end
843 end
823 end
844 end
General Comments 0
You need to be logged in to leave comments. Login now