##// END OF EJS Templates
Don't preload custom field filter values (#24787)....
Jean-Philippe Lang -
r15791:309c6cec861b
parent child
Show More
@@ -1,1231 +1,1226
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 QueryAssociationColumn < QueryColumn
77 class QueryAssociationColumn < QueryColumn
78
78
79 def initialize(association, attribute, options={})
79 def initialize(association, attribute, options={})
80 @association = association
80 @association = association
81 @attribute = attribute
81 @attribute = attribute
82 name_with_assoc = "#{association}.#{attribute}".to_sym
82 name_with_assoc = "#{association}.#{attribute}".to_sym
83 super(name_with_assoc, options)
83 super(name_with_assoc, options)
84 end
84 end
85
85
86 def value_object(object)
86 def value_object(object)
87 if assoc = object.send(@association)
87 if assoc = object.send(@association)
88 assoc.send @attribute
88 assoc.send @attribute
89 end
89 end
90 end
90 end
91
91
92 def css_classes
92 def css_classes
93 @css_classes ||= "#{@association}-#{@attribute}"
93 @css_classes ||= "#{@association}-#{@attribute}"
94 end
94 end
95 end
95 end
96
96
97 class QueryCustomFieldColumn < QueryColumn
97 class QueryCustomFieldColumn < QueryColumn
98
98
99 def initialize(custom_field, options={})
99 def initialize(custom_field, options={})
100 self.name = "cf_#{custom_field.id}".to_sym
100 self.name = "cf_#{custom_field.id}".to_sym
101 self.sortable = custom_field.order_statement || false
101 self.sortable = custom_field.order_statement || false
102 self.groupable = custom_field.group_statement || false
102 self.groupable = custom_field.group_statement || false
103 self.totalable = options.key?(:totalable) ? !!options[:totalable] : custom_field.totalable?
103 self.totalable = options.key?(:totalable) ? !!options[:totalable] : custom_field.totalable?
104 @inline = true
104 @inline = true
105 @cf = custom_field
105 @cf = custom_field
106 end
106 end
107
107
108 def caption
108 def caption
109 @cf.name
109 @cf.name
110 end
110 end
111
111
112 def custom_field
112 def custom_field
113 @cf
113 @cf
114 end
114 end
115
115
116 def value_object(object)
116 def value_object(object)
117 if custom_field.visible_by?(object.project, User.current)
117 if custom_field.visible_by?(object.project, User.current)
118 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
118 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
119 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
119 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
120 else
120 else
121 nil
121 nil
122 end
122 end
123 end
123 end
124
124
125 def value(object)
125 def value(object)
126 raw = value_object(object)
126 raw = value_object(object)
127 if raw.is_a?(Array)
127 if raw.is_a?(Array)
128 raw.map {|r| @cf.cast_value(r.value)}
128 raw.map {|r| @cf.cast_value(r.value)}
129 elsif raw
129 elsif raw
130 @cf.cast_value(raw.value)
130 @cf.cast_value(raw.value)
131 else
131 else
132 nil
132 nil
133 end
133 end
134 end
134 end
135
135
136 def css_classes
136 def css_classes
137 @css_classes ||= "#{name} #{@cf.field_format}"
137 @css_classes ||= "#{name} #{@cf.field_format}"
138 end
138 end
139 end
139 end
140
140
141 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
141 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
142
142
143 def initialize(association, custom_field, options={})
143 def initialize(association, custom_field, options={})
144 super(custom_field, options)
144 super(custom_field, options)
145 self.name = "#{association}.cf_#{custom_field.id}".to_sym
145 self.name = "#{association}.cf_#{custom_field.id}".to_sym
146 # TODO: support sorting/grouping by association custom field
146 # TODO: support sorting/grouping by association custom field
147 self.sortable = false
147 self.sortable = false
148 self.groupable = false
148 self.groupable = false
149 @association = association
149 @association = association
150 end
150 end
151
151
152 def value_object(object)
152 def value_object(object)
153 if assoc = object.send(@association)
153 if assoc = object.send(@association)
154 super(assoc)
154 super(assoc)
155 end
155 end
156 end
156 end
157
157
158 def css_classes
158 def css_classes
159 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
159 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
160 end
160 end
161 end
161 end
162
162
163 class QueryFilter
163 class QueryFilter
164 include Redmine::I18n
164 include Redmine::I18n
165
165
166 def initialize(field, options)
166 def initialize(field, options)
167 @field = field.to_s
167 @field = field.to_s
168 @options = options
168 @options = options
169 @options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
169 @options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
170 # Consider filters with a Proc for values as remote by default
170 # Consider filters with a Proc for values as remote by default
171 @remote = options.key?(:remote) ? options[:remote] : options[:values].is_a?(Proc)
171 @remote = options.key?(:remote) ? options[:remote] : options[:values].is_a?(Proc)
172 end
172 end
173
173
174 def [](arg)
174 def [](arg)
175 if arg == :values
175 if arg == :values
176 values
176 values
177 else
177 else
178 @options[arg]
178 @options[arg]
179 end
179 end
180 end
180 end
181
181
182 def values
182 def values
183 @values ||= begin
183 @values ||= begin
184 values = @options[:values]
184 values = @options[:values]
185 if values.is_a?(Proc)
185 if values.is_a?(Proc)
186 values = values.call
186 values = values.call
187 end
187 end
188 values
188 values
189 end
189 end
190 end
190 end
191
191
192 def remote
192 def remote
193 @remote
193 @remote
194 end
194 end
195 end
195 end
196
196
197 class Query < ActiveRecord::Base
197 class Query < ActiveRecord::Base
198 class StatementInvalid < ::ActiveRecord::StatementInvalid
198 class StatementInvalid < ::ActiveRecord::StatementInvalid
199 end
199 end
200
200
201 include Redmine::SubclassFactory
201 include Redmine::SubclassFactory
202
202
203 VISIBILITY_PRIVATE = 0
203 VISIBILITY_PRIVATE = 0
204 VISIBILITY_ROLES = 1
204 VISIBILITY_ROLES = 1
205 VISIBILITY_PUBLIC = 2
205 VISIBILITY_PUBLIC = 2
206
206
207 belongs_to :project
207 belongs_to :project
208 belongs_to :user
208 belongs_to :user
209 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
209 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
210 serialize :filters
210 serialize :filters
211 serialize :column_names
211 serialize :column_names
212 serialize :sort_criteria, Array
212 serialize :sort_criteria, Array
213 serialize :options, Hash
213 serialize :options, Hash
214
214
215 attr_protected :project_id, :user_id
215 attr_protected :project_id, :user_id
216
216
217 validates_presence_of :name
217 validates_presence_of :name
218 validates_length_of :name, :maximum => 255
218 validates_length_of :name, :maximum => 255
219 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
219 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
220 validate :validate_query_filters
220 validate :validate_query_filters
221 validate do |query|
221 validate do |query|
222 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
222 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
223 end
223 end
224
224
225 after_save do |query|
225 after_save do |query|
226 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
226 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
227 query.roles.clear
227 query.roles.clear
228 end
228 end
229 end
229 end
230
230
231 class_attribute :operators
231 class_attribute :operators
232 self.operators = {
232 self.operators = {
233 "=" => :label_equals,
233 "=" => :label_equals,
234 "!" => :label_not_equals,
234 "!" => :label_not_equals,
235 "o" => :label_open_issues,
235 "o" => :label_open_issues,
236 "c" => :label_closed_issues,
236 "c" => :label_closed_issues,
237 "!*" => :label_none,
237 "!*" => :label_none,
238 "*" => :label_any,
238 "*" => :label_any,
239 ">=" => :label_greater_or_equal,
239 ">=" => :label_greater_or_equal,
240 "<=" => :label_less_or_equal,
240 "<=" => :label_less_or_equal,
241 "><" => :label_between,
241 "><" => :label_between,
242 "<t+" => :label_in_less_than,
242 "<t+" => :label_in_less_than,
243 ">t+" => :label_in_more_than,
243 ">t+" => :label_in_more_than,
244 "><t+"=> :label_in_the_next_days,
244 "><t+"=> :label_in_the_next_days,
245 "t+" => :label_in,
245 "t+" => :label_in,
246 "t" => :label_today,
246 "t" => :label_today,
247 "ld" => :label_yesterday,
247 "ld" => :label_yesterday,
248 "w" => :label_this_week,
248 "w" => :label_this_week,
249 "lw" => :label_last_week,
249 "lw" => :label_last_week,
250 "l2w" => [:label_last_n_weeks, {:count => 2}],
250 "l2w" => [:label_last_n_weeks, {:count => 2}],
251 "m" => :label_this_month,
251 "m" => :label_this_month,
252 "lm" => :label_last_month,
252 "lm" => :label_last_month,
253 "y" => :label_this_year,
253 "y" => :label_this_year,
254 ">t-" => :label_less_than_ago,
254 ">t-" => :label_less_than_ago,
255 "<t-" => :label_more_than_ago,
255 "<t-" => :label_more_than_ago,
256 "><t-"=> :label_in_the_past_days,
256 "><t-"=> :label_in_the_past_days,
257 "t-" => :label_ago,
257 "t-" => :label_ago,
258 "~" => :label_contains,
258 "~" => :label_contains,
259 "!~" => :label_not_contains,
259 "!~" => :label_not_contains,
260 "=p" => :label_any_issues_in_project,
260 "=p" => :label_any_issues_in_project,
261 "=!p" => :label_any_issues_not_in_project,
261 "=!p" => :label_any_issues_not_in_project,
262 "!p" => :label_no_issues_in_project,
262 "!p" => :label_no_issues_in_project,
263 "*o" => :label_any_open_issues,
263 "*o" => :label_any_open_issues,
264 "!o" => :label_no_open_issues
264 "!o" => :label_no_open_issues
265 }
265 }
266
266
267 class_attribute :operators_by_filter_type
267 class_attribute :operators_by_filter_type
268 self.operators_by_filter_type = {
268 self.operators_by_filter_type = {
269 :list => [ "=", "!" ],
269 :list => [ "=", "!" ],
270 :list_status => [ "o", "=", "!", "c", "*" ],
270 :list_status => [ "o", "=", "!", "c", "*" ],
271 :list_optional => [ "=", "!", "!*", "*" ],
271 :list_optional => [ "=", "!", "!*", "*" ],
272 :list_subprojects => [ "*", "!*", "=" ],
272 :list_subprojects => [ "*", "!*", "=" ],
273 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
273 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
274 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
274 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
275 :string => [ "=", "~", "!", "!~", "!*", "*" ],
275 :string => [ "=", "~", "!", "!~", "!*", "*" ],
276 :text => [ "~", "!~", "!*", "*" ],
276 :text => [ "~", "!~", "!*", "*" ],
277 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
277 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
278 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
278 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
279 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
279 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
280 :tree => ["=", "~", "!*", "*"]
280 :tree => ["=", "~", "!*", "*"]
281 }
281 }
282
282
283 class_attribute :available_columns
283 class_attribute :available_columns
284 self.available_columns = []
284 self.available_columns = []
285
285
286 class_attribute :queried_class
286 class_attribute :queried_class
287
287
288 # Permission required to view the queries, set on subclasses.
288 # Permission required to view the queries, set on subclasses.
289 class_attribute :view_permission
289 class_attribute :view_permission
290
290
291 # Scope of queries that are global or on the given project
291 # Scope of queries that are global or on the given project
292 scope :global_or_on_project, lambda {|project|
292 scope :global_or_on_project, lambda {|project|
293 where(:project_id => (project.nil? ? nil : [nil, project.id]))
293 where(:project_id => (project.nil? ? nil : [nil, project.id]))
294 }
294 }
295
295
296 scope :sorted, lambda {order(:name, :id)}
296 scope :sorted, lambda {order(:name, :id)}
297
297
298 # Scope of visible queries, can be used from subclasses only.
298 # Scope of visible queries, can be used from subclasses only.
299 # Unlike other visible scopes, a class methods is used as it
299 # Unlike other visible scopes, a class methods is used as it
300 # let handle inheritance more nicely than scope DSL.
300 # let handle inheritance more nicely than scope DSL.
301 def self.visible(*args)
301 def self.visible(*args)
302 if self == ::Query
302 if self == ::Query
303 # Visibility depends on permissions for each subclass,
303 # Visibility depends on permissions for each subclass,
304 # raise an error if the scope is called from Query (eg. Query.visible)
304 # raise an error if the scope is called from Query (eg. Query.visible)
305 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
305 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
306 end
306 end
307
307
308 user = args.shift || User.current
308 user = args.shift || User.current
309 base = Project.allowed_to_condition(user, view_permission, *args)
309 base = Project.allowed_to_condition(user, view_permission, *args)
310 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
310 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
311 where("#{table_name}.project_id IS NULL OR (#{base})")
311 where("#{table_name}.project_id IS NULL OR (#{base})")
312
312
313 if user.admin?
313 if user.admin?
314 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
314 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
315 elsif user.memberships.any?
315 elsif user.memberships.any?
316 scope.where("#{table_name}.visibility = ?" +
316 scope.where("#{table_name}.visibility = ?" +
317 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
317 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
318 "SELECT DISTINCT q.id FROM #{table_name} q" +
318 "SELECT DISTINCT q.id FROM #{table_name} q" +
319 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
319 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
320 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
320 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
321 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
321 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
322 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
322 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
323 " OR #{table_name}.user_id = ?",
323 " OR #{table_name}.user_id = ?",
324 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
324 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
325 elsif user.logged?
325 elsif user.logged?
326 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
326 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
327 else
327 else
328 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
328 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
329 end
329 end
330 end
330 end
331
331
332 # Returns true if the query is visible to +user+ or the current user.
332 # Returns true if the query is visible to +user+ or the current user.
333 def visible?(user=User.current)
333 def visible?(user=User.current)
334 return true if user.admin?
334 return true if user.admin?
335 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
335 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
336 case visibility
336 case visibility
337 when VISIBILITY_PUBLIC
337 when VISIBILITY_PUBLIC
338 true
338 true
339 when VISIBILITY_ROLES
339 when VISIBILITY_ROLES
340 if project
340 if project
341 (user.roles_for_project(project) & roles).any?
341 (user.roles_for_project(project) & roles).any?
342 else
342 else
343 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
343 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
344 end
344 end
345 else
345 else
346 user == self.user
346 user == self.user
347 end
347 end
348 end
348 end
349
349
350 def is_private?
350 def is_private?
351 visibility == VISIBILITY_PRIVATE
351 visibility == VISIBILITY_PRIVATE
352 end
352 end
353
353
354 def is_public?
354 def is_public?
355 !is_private?
355 !is_private?
356 end
356 end
357
357
358 def queried_table_name
358 def queried_table_name
359 @queried_table_name ||= self.class.queried_class.table_name
359 @queried_table_name ||= self.class.queried_class.table_name
360 end
360 end
361
361
362 def initialize(attributes=nil, *args)
362 def initialize(attributes=nil, *args)
363 super attributes
363 super attributes
364 @is_for_all = project.nil?
364 @is_for_all = project.nil?
365 end
365 end
366
366
367 # Builds the query from the given params
367 # Builds the query from the given params
368 def build_from_params(params)
368 def build_from_params(params)
369 if params[:fields] || params[:f]
369 if params[:fields] || params[:f]
370 self.filters = {}
370 self.filters = {}
371 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
371 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
372 else
372 else
373 available_filters.keys.each do |field|
373 available_filters.keys.each do |field|
374 add_short_filter(field, params[field]) if params[field]
374 add_short_filter(field, params[field]) if params[field]
375 end
375 end
376 end
376 end
377 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
377 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
378 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
378 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
379 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
379 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
380 self
380 self
381 end
381 end
382
382
383 # Builds a new query from the given params and attributes
383 # Builds a new query from the given params and attributes
384 def self.build_from_params(params, attributes={})
384 def self.build_from_params(params, attributes={})
385 new(attributes).build_from_params(params)
385 new(attributes).build_from_params(params)
386 end
386 end
387
387
388 def validate_query_filters
388 def validate_query_filters
389 filters.each_key do |field|
389 filters.each_key do |field|
390 if values_for(field)
390 if values_for(field)
391 case type_for(field)
391 case type_for(field)
392 when :integer
392 when :integer
393 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
393 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
394 when :float
394 when :float
395 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
395 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
396 when :date, :date_past
396 when :date, :date_past
397 case operator_for(field)
397 case operator_for(field)
398 when "=", ">=", "<=", "><"
398 when "=", ">=", "<=", "><"
399 add_filter_error(field, :invalid) if values_for(field).detect {|v|
399 add_filter_error(field, :invalid) if values_for(field).detect {|v|
400 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
400 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
401 }
401 }
402 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
402 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
403 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
403 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
404 end
404 end
405 end
405 end
406 end
406 end
407
407
408 add_filter_error(field, :blank) unless
408 add_filter_error(field, :blank) unless
409 # filter requires one or more values
409 # filter requires one or more values
410 (values_for(field) and !values_for(field).first.blank?) or
410 (values_for(field) and !values_for(field).first.blank?) or
411 # filter doesn't require any value
411 # filter doesn't require any value
412 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
412 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
413 end if filters
413 end if filters
414 end
414 end
415
415
416 def add_filter_error(field, message)
416 def add_filter_error(field, message)
417 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
417 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
418 errors.add(:base, m)
418 errors.add(:base, m)
419 end
419 end
420
420
421 def editable_by?(user)
421 def editable_by?(user)
422 return false unless user
422 return false unless user
423 # Admin can edit them all and regular users can edit their private queries
423 # Admin can edit them all and regular users can edit their private queries
424 return true if user.admin? || (is_private? && self.user_id == user.id)
424 return true if user.admin? || (is_private? && self.user_id == user.id)
425 # Members can not edit public queries that are for all project (only admin is allowed to)
425 # Members can not edit public queries that are for all project (only admin is allowed to)
426 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
426 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
427 end
427 end
428
428
429 def trackers
429 def trackers
430 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
430 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
431 end
431 end
432
432
433 # Returns a hash of localized labels for all filter operators
433 # Returns a hash of localized labels for all filter operators
434 def self.operators_labels
434 def self.operators_labels
435 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
435 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
436 end
436 end
437
437
438 # Returns a representation of the available filters for JSON serialization
438 # Returns a representation of the available filters for JSON serialization
439 def available_filters_as_json
439 def available_filters_as_json
440 json = {}
440 json = {}
441 available_filters.each do |field, filter|
441 available_filters.each do |field, filter|
442 options = {:type => filter[:type], :name => filter[:name]}
442 options = {:type => filter[:type], :name => filter[:name]}
443 options[:remote] = true if filter.remote
443 options[:remote] = true if filter.remote
444
444
445 if has_filter?(field) || !filter.remote
445 if has_filter?(field) || !filter.remote
446 options[:values] = filter.values
446 options[:values] = filter.values
447 if options[:values] && values_for(field)
447 if options[:values] && values_for(field)
448 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
448 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
449 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
449 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
450 options[:values] += send(method, missing)
450 options[:values] += send(method, missing)
451 end
451 end
452 end
452 end
453 end
453 end
454 json[field] = options.stringify_keys
454 json[field] = options.stringify_keys
455 end
455 end
456 json
456 json
457 end
457 end
458
458
459 def all_projects
459 def all_projects
460 @all_projects ||= Project.visible.to_a
460 @all_projects ||= Project.visible.to_a
461 end
461 end
462
462
463 def all_projects_values
463 def all_projects_values
464 return @all_projects_values if @all_projects_values
464 return @all_projects_values if @all_projects_values
465
465
466 values = []
466 values = []
467 Project.project_tree(all_projects) do |p, level|
467 Project.project_tree(all_projects) do |p, level|
468 prefix = (level > 0 ? ('--' * level + ' ') : '')
468 prefix = (level > 0 ? ('--' * level + ' ') : '')
469 values << ["#{prefix}#{p.name}", p.id.to_s]
469 values << ["#{prefix}#{p.name}", p.id.to_s]
470 end
470 end
471 @all_projects_values = values
471 @all_projects_values = values
472 end
472 end
473
473
474 def project_values
474 def project_values
475 project_values = []
475 project_values = []
476 if User.current.logged? && User.current.memberships.any?
476 if User.current.logged? && User.current.memberships.any?
477 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
477 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
478 end
478 end
479 project_values += all_projects_values
479 project_values += all_projects_values
480 project_values
480 project_values
481 end
481 end
482
482
483 def subproject_values
483 def subproject_values
484 project.descendants.visible.collect{|s| [s.name, s.id.to_s] }
484 project.descendants.visible.collect{|s| [s.name, s.id.to_s] }
485 end
485 end
486
486
487 def principals
487 def principals
488 @principal ||= begin
488 @principal ||= begin
489 principals = []
489 principals = []
490 if project
490 if project
491 principals += project.principals.visible
491 principals += project.principals.visible
492 unless project.leaf?
492 unless project.leaf?
493 principals += Principal.member_of(project.descendants.visible).visible
493 principals += Principal.member_of(project.descendants.visible).visible
494 end
494 end
495 else
495 else
496 principals += Principal.member_of(all_projects).visible
496 principals += Principal.member_of(all_projects).visible
497 end
497 end
498 principals.uniq!
498 principals.uniq!
499 principals.sort!
499 principals.sort!
500 principals.reject! {|p| p.is_a?(GroupBuiltin)}
500 principals.reject! {|p| p.is_a?(GroupBuiltin)}
501 principals
501 principals
502 end
502 end
503 end
503 end
504
504
505 def users
505 def users
506 principals.select {|p| p.is_a?(User)}
506 principals.select {|p| p.is_a?(User)}
507 end
507 end
508
508
509 def author_values
509 def author_values
510 author_values = []
510 author_values = []
511 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
511 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
512 author_values += users.collect{|s| [s.name, s.id.to_s] }
512 author_values += users.collect{|s| [s.name, s.id.to_s] }
513 author_values
513 author_values
514 end
514 end
515
515
516 def assigned_to_values
516 def assigned_to_values
517 assigned_to_values = []
517 assigned_to_values = []
518 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
518 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
519 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
519 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
520 assigned_to_values
520 assigned_to_values
521 end
521 end
522
522
523 def fixed_version_values
523 def fixed_version_values
524 versions = []
524 versions = []
525 if project
525 if project
526 versions = project.shared_versions.to_a
526 versions = project.shared_versions.to_a
527 else
527 else
528 versions = Version.visible.where(:sharing => 'system').to_a
528 versions = Version.visible.where(:sharing => 'system').to_a
529 end
529 end
530 Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] }
530 Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] }
531 end
531 end
532
532
533 # Adds available filters
533 # Adds available filters
534 def initialize_available_filters
534 def initialize_available_filters
535 # implemented by sub-classes
535 # implemented by sub-classes
536 end
536 end
537 protected :initialize_available_filters
537 protected :initialize_available_filters
538
538
539 # Adds an available filter
539 # Adds an available filter
540 def add_available_filter(field, options)
540 def add_available_filter(field, options)
541 @available_filters ||= ActiveSupport::OrderedHash.new
541 @available_filters ||= ActiveSupport::OrderedHash.new
542 @available_filters[field] = QueryFilter.new(field, options)
542 @available_filters[field] = QueryFilter.new(field, options)
543 @available_filters
543 @available_filters
544 end
544 end
545
545
546 # Removes an available filter
546 # Removes an available filter
547 def delete_available_filter(field)
547 def delete_available_filter(field)
548 if @available_filters
548 if @available_filters
549 @available_filters.delete(field)
549 @available_filters.delete(field)
550 end
550 end
551 end
551 end
552
552
553 # Return a hash of available filters
553 # Return a hash of available filters
554 def available_filters
554 def available_filters
555 unless @available_filters
555 unless @available_filters
556 initialize_available_filters
556 initialize_available_filters
557 @available_filters ||= {}
557 @available_filters ||= {}
558 end
558 end
559 @available_filters
559 @available_filters
560 end
560 end
561
561
562 def add_filter(field, operator, values=nil)
562 def add_filter(field, operator, values=nil)
563 # values must be an array
563 # values must be an array
564 return unless values.nil? || values.is_a?(Array)
564 return unless values.nil? || values.is_a?(Array)
565 # check if field is defined as an available filter
565 # check if field is defined as an available filter
566 if available_filters.has_key? field
566 if available_filters.has_key? field
567 filter_options = available_filters[field]
567 filter_options = available_filters[field]
568 filters[field] = {:operator => operator, :values => (values || [''])}
568 filters[field] = {:operator => operator, :values => (values || [''])}
569 end
569 end
570 end
570 end
571
571
572 def add_short_filter(field, expression)
572 def add_short_filter(field, expression)
573 return unless expression && available_filters.has_key?(field)
573 return unless expression && available_filters.has_key?(field)
574 field_type = available_filters[field][:type]
574 field_type = available_filters[field][:type]
575 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
575 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
576 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
576 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
577 values = $1
577 values = $1
578 add_filter field, operator, values.present? ? values.split('|') : ['']
578 add_filter field, operator, values.present? ? values.split('|') : ['']
579 end || add_filter(field, '=', expression.to_s.split('|'))
579 end || add_filter(field, '=', expression.to_s.split('|'))
580 end
580 end
581
581
582 # Add multiple filters using +add_filter+
582 # Add multiple filters using +add_filter+
583 def add_filters(fields, operators, values)
583 def add_filters(fields, operators, values)
584 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
584 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
585 fields.each do |field|
585 fields.each do |field|
586 add_filter(field, operators[field], values && values[field])
586 add_filter(field, operators[field], values && values[field])
587 end
587 end
588 end
588 end
589 end
589 end
590
590
591 def has_filter?(field)
591 def has_filter?(field)
592 filters and filters[field]
592 filters and filters[field]
593 end
593 end
594
594
595 def type_for(field)
595 def type_for(field)
596 available_filters[field][:type] if available_filters.has_key?(field)
596 available_filters[field][:type] if available_filters.has_key?(field)
597 end
597 end
598
598
599 def operator_for(field)
599 def operator_for(field)
600 has_filter?(field) ? filters[field][:operator] : nil
600 has_filter?(field) ? filters[field][:operator] : nil
601 end
601 end
602
602
603 def values_for(field)
603 def values_for(field)
604 has_filter?(field) ? filters[field][:values] : nil
604 has_filter?(field) ? filters[field][:values] : nil
605 end
605 end
606
606
607 def value_for(field, index=0)
607 def value_for(field, index=0)
608 (values_for(field) || [])[index]
608 (values_for(field) || [])[index]
609 end
609 end
610
610
611 def label_for(field)
611 def label_for(field)
612 label = available_filters[field][:name] if available_filters.has_key?(field)
612 label = available_filters[field][:name] if available_filters.has_key?(field)
613 label ||= queried_class.human_attribute_name(field, :default => field)
613 label ||= queried_class.human_attribute_name(field, :default => field)
614 end
614 end
615
615
616 def self.add_available_column(column)
616 def self.add_available_column(column)
617 self.available_columns << (column) if column.is_a?(QueryColumn)
617 self.available_columns << (column) if column.is_a?(QueryColumn)
618 end
618 end
619
619
620 # Returns an array of columns that can be used to group the results
620 # Returns an array of columns that can be used to group the results
621 def groupable_columns
621 def groupable_columns
622 available_columns.select {|c| c.groupable}
622 available_columns.select {|c| c.groupable}
623 end
623 end
624
624
625 # Returns a Hash of columns and the key for sorting
625 # Returns a Hash of columns and the key for sorting
626 def sortable_columns
626 def sortable_columns
627 available_columns.inject({}) {|h, column|
627 available_columns.inject({}) {|h, column|
628 h[column.name.to_s] = column.sortable
628 h[column.name.to_s] = column.sortable
629 h
629 h
630 }
630 }
631 end
631 end
632
632
633 def columns
633 def columns
634 # preserve the column_names order
634 # preserve the column_names order
635 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
635 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
636 available_columns.find { |col| col.name == name }
636 available_columns.find { |col| col.name == name }
637 end.compact
637 end.compact
638 available_columns.select(&:frozen?) | cols
638 available_columns.select(&:frozen?) | cols
639 end
639 end
640
640
641 def inline_columns
641 def inline_columns
642 columns.select(&:inline?)
642 columns.select(&:inline?)
643 end
643 end
644
644
645 def block_columns
645 def block_columns
646 columns.reject(&:inline?)
646 columns.reject(&:inline?)
647 end
647 end
648
648
649 def available_inline_columns
649 def available_inline_columns
650 available_columns.select(&:inline?)
650 available_columns.select(&:inline?)
651 end
651 end
652
652
653 def available_block_columns
653 def available_block_columns
654 available_columns.reject(&:inline?)
654 available_columns.reject(&:inline?)
655 end
655 end
656
656
657 def available_totalable_columns
657 def available_totalable_columns
658 available_columns.select(&:totalable)
658 available_columns.select(&:totalable)
659 end
659 end
660
660
661 def default_columns_names
661 def default_columns_names
662 []
662 []
663 end
663 end
664
664
665 def default_totalable_names
665 def default_totalable_names
666 []
666 []
667 end
667 end
668
668
669 def column_names=(names)
669 def column_names=(names)
670 if names
670 if names
671 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
671 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
672 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
672 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
673 # Set column_names to nil if default columns
673 # Set column_names to nil if default columns
674 if names == default_columns_names
674 if names == default_columns_names
675 names = nil
675 names = nil
676 end
676 end
677 end
677 end
678 write_attribute(:column_names, names)
678 write_attribute(:column_names, names)
679 end
679 end
680
680
681 def has_column?(column)
681 def has_column?(column)
682 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
682 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
683 end
683 end
684
684
685 def has_custom_field_column?
685 def has_custom_field_column?
686 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
686 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
687 end
687 end
688
688
689 def has_default_columns?
689 def has_default_columns?
690 column_names.nil? || column_names.empty?
690 column_names.nil? || column_names.empty?
691 end
691 end
692
692
693 def totalable_columns
693 def totalable_columns
694 names = totalable_names
694 names = totalable_names
695 available_totalable_columns.select {|column| names.include?(column.name)}
695 available_totalable_columns.select {|column| names.include?(column.name)}
696 end
696 end
697
697
698 def totalable_names=(names)
698 def totalable_names=(names)
699 if names
699 if names
700 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
700 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
701 end
701 end
702 options[:totalable_names] = names
702 options[:totalable_names] = names
703 end
703 end
704
704
705 def totalable_names
705 def totalable_names
706 options[:totalable_names] || default_totalable_names || []
706 options[:totalable_names] || default_totalable_names || []
707 end
707 end
708
708
709 def sort_criteria=(arg)
709 def sort_criteria=(arg)
710 c = []
710 c = []
711 if arg.is_a?(Hash)
711 if arg.is_a?(Hash)
712 arg = arg.keys.sort.collect {|k| arg[k]}
712 arg = arg.keys.sort.collect {|k| arg[k]}
713 end
713 end
714 if arg
714 if arg
715 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
715 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
716 end
716 end
717 write_attribute(:sort_criteria, c)
717 write_attribute(:sort_criteria, c)
718 end
718 end
719
719
720 def sort_criteria
720 def sort_criteria
721 read_attribute(:sort_criteria) || []
721 read_attribute(:sort_criteria) || []
722 end
722 end
723
723
724 def sort_criteria_key(arg)
724 def sort_criteria_key(arg)
725 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
725 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
726 end
726 end
727
727
728 def sort_criteria_order(arg)
728 def sort_criteria_order(arg)
729 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
729 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
730 end
730 end
731
731
732 def sort_criteria_order_for(key)
732 def sort_criteria_order_for(key)
733 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
733 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
734 end
734 end
735
735
736 # Returns the SQL sort order that should be prepended for grouping
736 # Returns the SQL sort order that should be prepended for grouping
737 def group_by_sort_order
737 def group_by_sort_order
738 if column = group_by_column
738 if column = group_by_column
739 order = (sort_criteria_order_for(column.name) || column.default_order || 'asc').try(:upcase)
739 order = (sort_criteria_order_for(column.name) || column.default_order || 'asc').try(:upcase)
740 Array(column.sortable).map {|s| "#{s} #{order}"}
740 Array(column.sortable).map {|s| "#{s} #{order}"}
741 end
741 end
742 end
742 end
743
743
744 # Returns true if the query is a grouped query
744 # Returns true if the query is a grouped query
745 def grouped?
745 def grouped?
746 !group_by_column.nil?
746 !group_by_column.nil?
747 end
747 end
748
748
749 def group_by_column
749 def group_by_column
750 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
750 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
751 end
751 end
752
752
753 def group_by_statement
753 def group_by_statement
754 group_by_column.try(:groupable)
754 group_by_column.try(:groupable)
755 end
755 end
756
756
757 def project_statement
757 def project_statement
758 project_clauses = []
758 project_clauses = []
759 if project && !project.descendants.active.empty?
759 if project && !project.descendants.active.empty?
760 if has_filter?("subproject_id")
760 if has_filter?("subproject_id")
761 case operator_for("subproject_id")
761 case operator_for("subproject_id")
762 when '='
762 when '='
763 # include the selected subprojects
763 # include the selected subprojects
764 ids = [project.id] + values_for("subproject_id").each(&:to_i)
764 ids = [project.id] + values_for("subproject_id").each(&:to_i)
765 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
765 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
766 when '!*'
766 when '!*'
767 # main project only
767 # main project only
768 project_clauses << "#{Project.table_name}.id = %d" % project.id
768 project_clauses << "#{Project.table_name}.id = %d" % project.id
769 else
769 else
770 # all subprojects
770 # all subprojects
771 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
771 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
772 end
772 end
773 elsif Setting.display_subprojects_issues?
773 elsif Setting.display_subprojects_issues?
774 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
774 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
775 else
775 else
776 project_clauses << "#{Project.table_name}.id = %d" % project.id
776 project_clauses << "#{Project.table_name}.id = %d" % project.id
777 end
777 end
778 elsif project
778 elsif project
779 project_clauses << "#{Project.table_name}.id = %d" % project.id
779 project_clauses << "#{Project.table_name}.id = %d" % project.id
780 end
780 end
781 project_clauses.any? ? project_clauses.join(' AND ') : nil
781 project_clauses.any? ? project_clauses.join(' AND ') : nil
782 end
782 end
783
783
784 def statement
784 def statement
785 # filters clauses
785 # filters clauses
786 filters_clauses = []
786 filters_clauses = []
787 filters.each_key do |field|
787 filters.each_key do |field|
788 next if field == "subproject_id"
788 next if field == "subproject_id"
789 v = values_for(field).clone
789 v = values_for(field).clone
790 next unless v and !v.empty?
790 next unless v and !v.empty?
791 operator = operator_for(field)
791 operator = operator_for(field)
792
792
793 # "me" value substitution
793 # "me" value substitution
794 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
794 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
795 if v.delete("me")
795 if v.delete("me")
796 if User.current.logged?
796 if User.current.logged?
797 v.push(User.current.id.to_s)
797 v.push(User.current.id.to_s)
798 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
798 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
799 else
799 else
800 v.push("0")
800 v.push("0")
801 end
801 end
802 end
802 end
803 end
803 end
804
804
805 if field == 'project_id'
805 if field == 'project_id'
806 if v.delete('mine')
806 if v.delete('mine')
807 v += User.current.memberships.map(&:project_id).map(&:to_s)
807 v += User.current.memberships.map(&:project_id).map(&:to_s)
808 end
808 end
809 end
809 end
810
810
811 if field =~ /cf_(\d+)$/
811 if field =~ /cf_(\d+)$/
812 # custom field
812 # custom field
813 filters_clauses << sql_for_custom_field(field, operator, v, $1)
813 filters_clauses << sql_for_custom_field(field, operator, v, $1)
814 elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
814 elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
815 # specific statement
815 # specific statement
816 filters_clauses << send(method, field, operator, v)
816 filters_clauses << send(method, field, operator, v)
817 else
817 else
818 # regular field
818 # regular field
819 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
819 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
820 end
820 end
821 end if filters and valid?
821 end if filters and valid?
822
822
823 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
823 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
824 # Excludes results for which the grouped custom field is not visible
824 # Excludes results for which the grouped custom field is not visible
825 filters_clauses << c.custom_field.visibility_by_project_condition
825 filters_clauses << c.custom_field.visibility_by_project_condition
826 end
826 end
827
827
828 filters_clauses << project_statement
828 filters_clauses << project_statement
829 filters_clauses.reject!(&:blank?)
829 filters_clauses.reject!(&:blank?)
830
830
831 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
831 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
832 end
832 end
833
833
834 # Returns the sum of values for the given column
834 # Returns the sum of values for the given column
835 def total_for(column)
835 def total_for(column)
836 total_with_scope(column, base_scope)
836 total_with_scope(column, base_scope)
837 end
837 end
838
838
839 # Returns a hash of the sum of the given column for each group,
839 # Returns a hash of the sum of the given column for each group,
840 # or nil if the query is not grouped
840 # or nil if the query is not grouped
841 def total_by_group_for(column)
841 def total_by_group_for(column)
842 grouped_query do |scope|
842 grouped_query do |scope|
843 total_with_scope(column, scope)
843 total_with_scope(column, scope)
844 end
844 end
845 end
845 end
846
846
847 def totals
847 def totals
848 totals = totalable_columns.map {|column| [column, total_for(column)]}
848 totals = totalable_columns.map {|column| [column, total_for(column)]}
849 yield totals if block_given?
849 yield totals if block_given?
850 totals
850 totals
851 end
851 end
852
852
853 def totals_by_group
853 def totals_by_group
854 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
854 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
855 yield totals if block_given?
855 yield totals if block_given?
856 totals
856 totals
857 end
857 end
858
858
859 private
859 private
860
860
861 def grouped_query(&block)
861 def grouped_query(&block)
862 r = nil
862 r = nil
863 if grouped?
863 if grouped?
864 begin
864 begin
865 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
865 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
866 r = yield base_group_scope
866 r = yield base_group_scope
867 rescue ActiveRecord::RecordNotFound
867 rescue ActiveRecord::RecordNotFound
868 r = {nil => yield(base_scope)}
868 r = {nil => yield(base_scope)}
869 end
869 end
870 c = group_by_column
870 c = group_by_column
871 if c.is_a?(QueryCustomFieldColumn)
871 if c.is_a?(QueryCustomFieldColumn)
872 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
872 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
873 end
873 end
874 end
874 end
875 r
875 r
876 rescue ::ActiveRecord::StatementInvalid => e
876 rescue ::ActiveRecord::StatementInvalid => e
877 raise StatementInvalid.new(e.message)
877 raise StatementInvalid.new(e.message)
878 end
878 end
879
879
880 def total_with_scope(column, scope)
880 def total_with_scope(column, scope)
881 unless column.is_a?(QueryColumn)
881 unless column.is_a?(QueryColumn)
882 column = column.to_sym
882 column = column.to_sym
883 column = available_totalable_columns.detect {|c| c.name == column}
883 column = available_totalable_columns.detect {|c| c.name == column}
884 end
884 end
885 if column.is_a?(QueryCustomFieldColumn)
885 if column.is_a?(QueryCustomFieldColumn)
886 custom_field = column.custom_field
886 custom_field = column.custom_field
887 send "total_for_custom_field", custom_field, scope
887 send "total_for_custom_field", custom_field, scope
888 else
888 else
889 send "total_for_#{column.name}", scope
889 send "total_for_#{column.name}", scope
890 end
890 end
891 rescue ::ActiveRecord::StatementInvalid => e
891 rescue ::ActiveRecord::StatementInvalid => e
892 raise StatementInvalid.new(e.message)
892 raise StatementInvalid.new(e.message)
893 end
893 end
894
894
895 def base_scope
895 def base_scope
896 raise "unimplemented"
896 raise "unimplemented"
897 end
897 end
898
898
899 def base_group_scope
899 def base_group_scope
900 base_scope.
900 base_scope.
901 joins(joins_for_order_statement(group_by_statement)).
901 joins(joins_for_order_statement(group_by_statement)).
902 group(group_by_statement)
902 group(group_by_statement)
903 end
903 end
904
904
905 def total_for_custom_field(custom_field, scope, &block)
905 def total_for_custom_field(custom_field, scope, &block)
906 total = custom_field.format.total_for_scope(custom_field, scope)
906 total = custom_field.format.total_for_scope(custom_field, scope)
907 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
907 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
908 total
908 total
909 end
909 end
910
910
911 def map_total(total, &block)
911 def map_total(total, &block)
912 if total.is_a?(Hash)
912 if total.is_a?(Hash)
913 total.keys.each {|k| total[k] = yield total[k]}
913 total.keys.each {|k| total[k] = yield total[k]}
914 else
914 else
915 total = yield total
915 total = yield total
916 end
916 end
917 total
917 total
918 end
918 end
919
919
920 def sql_for_custom_field(field, operator, value, custom_field_id)
920 def sql_for_custom_field(field, operator, value, custom_field_id)
921 db_table = CustomValue.table_name
921 db_table = CustomValue.table_name
922 db_field = 'value'
922 db_field = 'value'
923 filter = @available_filters[field]
923 filter = @available_filters[field]
924 return nil unless filter
924 return nil unless filter
925 if filter[:field].format.target_class && filter[:field].format.target_class <= User
925 if filter[:field].format.target_class && filter[:field].format.target_class <= User
926 if value.delete('me')
926 if value.delete('me')
927 value.push User.current.id.to_s
927 value.push User.current.id.to_s
928 end
928 end
929 end
929 end
930 not_in = nil
930 not_in = nil
931 if operator == '!'
931 if operator == '!'
932 # Makes ! operator work for custom fields with multiple values
932 # Makes ! operator work for custom fields with multiple values
933 operator = '='
933 operator = '='
934 not_in = 'NOT'
934 not_in = 'NOT'
935 end
935 end
936 customized_key = "id"
936 customized_key = "id"
937 customized_class = queried_class
937 customized_class = queried_class
938 if field =~ /^(.+)\.cf_/
938 if field =~ /^(.+)\.cf_/
939 assoc = $1
939 assoc = $1
940 customized_key = "#{assoc}_id"
940 customized_key = "#{assoc}_id"
941 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
941 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
942 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
942 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
943 end
943 end
944 where = sql_for_field(field, operator, value, db_table, db_field, true)
944 where = sql_for_field(field, operator, value, db_table, db_field, true)
945 if operator =~ /[<>]/
945 if operator =~ /[<>]/
946 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
946 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
947 end
947 end
948 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
948 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
949 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
949 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
950 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
950 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
951 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
951 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
952 end
952 end
953
953
954 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
954 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
955 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
955 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
956 sql = ''
956 sql = ''
957 case operator
957 case operator
958 when "="
958 when "="
959 if value.any?
959 if value.any?
960 case type_for(field)
960 case type_for(field)
961 when :date, :date_past
961 when :date, :date_past
962 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
962 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
963 when :integer
963 when :integer
964 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
964 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
965 if int_values.present?
965 if int_values.present?
966 if is_custom_filter
966 if is_custom_filter
967 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) IN (#{int_values}))"
967 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) IN (#{int_values}))"
968 else
968 else
969 sql = "#{db_table}.#{db_field} IN (#{int_values})"
969 sql = "#{db_table}.#{db_field} IN (#{int_values})"
970 end
970 end
971 else
971 else
972 sql = "1=0"
972 sql = "1=0"
973 end
973 end
974 when :float
974 when :float
975 if is_custom_filter
975 if is_custom_filter
976 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
976 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
977 else
977 else
978 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
978 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
979 end
979 end
980 else
980 else
981 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
981 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
982 end
982 end
983 else
983 else
984 # IN an empty set
984 # IN an empty set
985 sql = "1=0"
985 sql = "1=0"
986 end
986 end
987 when "!"
987 when "!"
988 if value.any?
988 if value.any?
989 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
989 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
990 else
990 else
991 # NOT IN an empty set
991 # NOT IN an empty set
992 sql = "1=1"
992 sql = "1=1"
993 end
993 end
994 when "!*"
994 when "!*"
995 sql = "#{db_table}.#{db_field} IS NULL"
995 sql = "#{db_table}.#{db_field} IS NULL"
996 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
996 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
997 when "*"
997 when "*"
998 sql = "#{db_table}.#{db_field} IS NOT NULL"
998 sql = "#{db_table}.#{db_field} IS NOT NULL"
999 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
999 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
1000 when ">="
1000 when ">="
1001 if [:date, :date_past].include?(type_for(field))
1001 if [:date, :date_past].include?(type_for(field))
1002 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
1002 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
1003 else
1003 else
1004 if is_custom_filter
1004 if is_custom_filter
1005 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
1005 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
1006 else
1006 else
1007 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
1007 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
1008 end
1008 end
1009 end
1009 end
1010 when "<="
1010 when "<="
1011 if [:date, :date_past].include?(type_for(field))
1011 if [:date, :date_past].include?(type_for(field))
1012 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
1012 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
1013 else
1013 else
1014 if is_custom_filter
1014 if is_custom_filter
1015 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
1015 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
1016 else
1016 else
1017 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
1017 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
1018 end
1018 end
1019 end
1019 end
1020 when "><"
1020 when "><"
1021 if [:date, :date_past].include?(type_for(field))
1021 if [:date, :date_past].include?(type_for(field))
1022 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
1022 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
1023 else
1023 else
1024 if is_custom_filter
1024 if is_custom_filter
1025 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
1025 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
1026 else
1026 else
1027 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
1027 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
1028 end
1028 end
1029 end
1029 end
1030 when "o"
1030 when "o"
1031 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
1031 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
1032 when "c"
1032 when "c"
1033 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
1033 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
1034 when "><t-"
1034 when "><t-"
1035 # between today - n days and today
1035 # between today - n days and today
1036 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
1036 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
1037 when ">t-"
1037 when ">t-"
1038 # >= today - n days
1038 # >= today - n days
1039 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
1039 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
1040 when "<t-"
1040 when "<t-"
1041 # <= today - n days
1041 # <= today - n days
1042 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
1042 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
1043 when "t-"
1043 when "t-"
1044 # = n days in past
1044 # = n days in past
1045 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
1045 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
1046 when "><t+"
1046 when "><t+"
1047 # between today and today + n days
1047 # between today and today + n days
1048 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
1048 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
1049 when ">t+"
1049 when ">t+"
1050 # >= today + n days
1050 # >= today + n days
1051 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
1051 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
1052 when "<t+"
1052 when "<t+"
1053 # <= today + n days
1053 # <= today + n days
1054 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
1054 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
1055 when "t+"
1055 when "t+"
1056 # = today + n days
1056 # = today + n days
1057 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
1057 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
1058 when "t"
1058 when "t"
1059 # = today
1059 # = today
1060 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
1060 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
1061 when "ld"
1061 when "ld"
1062 # = yesterday
1062 # = yesterday
1063 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
1063 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
1064 when "w"
1064 when "w"
1065 # = this week
1065 # = this week
1066 first_day_of_week = l(:general_first_day_of_week).to_i
1066 first_day_of_week = l(:general_first_day_of_week).to_i
1067 day_of_week = User.current.today.cwday
1067 day_of_week = User.current.today.cwday
1068 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1068 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1069 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
1069 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
1070 when "lw"
1070 when "lw"
1071 # = last week
1071 # = last week
1072 first_day_of_week = l(:general_first_day_of_week).to_i
1072 first_day_of_week = l(:general_first_day_of_week).to_i
1073 day_of_week = User.current.today.cwday
1073 day_of_week = User.current.today.cwday
1074 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1074 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1075 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
1075 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
1076 when "l2w"
1076 when "l2w"
1077 # = last 2 weeks
1077 # = last 2 weeks
1078 first_day_of_week = l(:general_first_day_of_week).to_i
1078 first_day_of_week = l(:general_first_day_of_week).to_i
1079 day_of_week = User.current.today.cwday
1079 day_of_week = User.current.today.cwday
1080 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1080 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1081 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
1081 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
1082 when "m"
1082 when "m"
1083 # = this month
1083 # = this month
1084 date = User.current.today
1084 date = User.current.today
1085 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1085 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1086 when "lm"
1086 when "lm"
1087 # = last month
1087 # = last month
1088 date = User.current.today.prev_month
1088 date = User.current.today.prev_month
1089 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1089 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1090 when "y"
1090 when "y"
1091 # = this year
1091 # = this year
1092 date = User.current.today
1092 date = User.current.today
1093 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
1093 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
1094 when "~"
1094 when "~"
1095 sql = sql_contains("#{db_table}.#{db_field}", value.first)
1095 sql = sql_contains("#{db_table}.#{db_field}", value.first)
1096 when "!~"
1096 when "!~"
1097 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
1097 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
1098 else
1098 else
1099 raise "Unknown query operator #{operator}"
1099 raise "Unknown query operator #{operator}"
1100 end
1100 end
1101
1101
1102 return sql
1102 return sql
1103 end
1103 end
1104
1104
1105 # Returns a SQL LIKE statement with wildcards
1105 # Returns a SQL LIKE statement with wildcards
1106 def sql_contains(db_field, value, match=true)
1106 def sql_contains(db_field, value, match=true)
1107 queried_class.send :sanitize_sql_for_conditions,
1107 queried_class.send :sanitize_sql_for_conditions,
1108 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
1108 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
1109 end
1109 end
1110
1110
1111 # Adds a filter for the given custom field
1111 # Adds a filter for the given custom field
1112 def add_custom_field_filter(field, assoc=nil)
1112 def add_custom_field_filter(field, assoc=nil)
1113 options = field.query_filter_options(self)
1113 options = field.query_filter_options(self)
1114 if field.format.target_class && field.format.target_class <= User
1115 if options[:values].is_a?(Array) && User.current.logged?
1116 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
1117 end
1118 end
1119
1114
1120 filter_id = "cf_#{field.id}"
1115 filter_id = "cf_#{field.id}"
1121 filter_name = field.name
1116 filter_name = field.name
1122 if assoc.present?
1117 if assoc.present?
1123 filter_id = "#{assoc}.#{filter_id}"
1118 filter_id = "#{assoc}.#{filter_id}"
1124 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1119 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1125 end
1120 end
1126 add_available_filter filter_id, options.merge({
1121 add_available_filter filter_id, options.merge({
1127 :name => filter_name,
1122 :name => filter_name,
1128 :field => field
1123 :field => field
1129 })
1124 })
1130 end
1125 end
1131
1126
1132 # Adds filters for the given custom fields scope
1127 # Adds filters for the given custom fields scope
1133 def add_custom_fields_filters(scope, assoc=nil)
1128 def add_custom_fields_filters(scope, assoc=nil)
1134 scope.visible.where(:is_filter => true).sorted.each do |field|
1129 scope.visible.where(:is_filter => true).sorted.each do |field|
1135 add_custom_field_filter(field, assoc)
1130 add_custom_field_filter(field, assoc)
1136 end
1131 end
1137 end
1132 end
1138
1133
1139 # Adds filters for the given associations custom fields
1134 # Adds filters for the given associations custom fields
1140 def add_associations_custom_fields_filters(*associations)
1135 def add_associations_custom_fields_filters(*associations)
1141 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1136 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1142 associations.each do |assoc|
1137 associations.each do |assoc|
1143 association_klass = queried_class.reflect_on_association(assoc).klass
1138 association_klass = queried_class.reflect_on_association(assoc).klass
1144 fields_by_class.each do |field_class, fields|
1139 fields_by_class.each do |field_class, fields|
1145 if field_class.customized_class <= association_klass
1140 if field_class.customized_class <= association_klass
1146 fields.sort.each do |field|
1141 fields.sort.each do |field|
1147 add_custom_field_filter(field, assoc)
1142 add_custom_field_filter(field, assoc)
1148 end
1143 end
1149 end
1144 end
1150 end
1145 end
1151 end
1146 end
1152 end
1147 end
1153
1148
1154 def quoted_time(time, is_custom_filter)
1149 def quoted_time(time, is_custom_filter)
1155 if is_custom_filter
1150 if is_custom_filter
1156 # Custom field values are stored as strings in the DB
1151 # Custom field values are stored as strings in the DB
1157 # using this format that does not depend on DB date representation
1152 # using this format that does not depend on DB date representation
1158 time.strftime("%Y-%m-%d %H:%M:%S")
1153 time.strftime("%Y-%m-%d %H:%M:%S")
1159 else
1154 else
1160 self.class.connection.quoted_date(time)
1155 self.class.connection.quoted_date(time)
1161 end
1156 end
1162 end
1157 end
1163
1158
1164 def date_for_user_time_zone(y, m, d)
1159 def date_for_user_time_zone(y, m, d)
1165 if tz = User.current.time_zone
1160 if tz = User.current.time_zone
1166 tz.local y, m, d
1161 tz.local y, m, d
1167 else
1162 else
1168 Time.local y, m, d
1163 Time.local y, m, d
1169 end
1164 end
1170 end
1165 end
1171
1166
1172 # Returns a SQL clause for a date or datetime field.
1167 # Returns a SQL clause for a date or datetime field.
1173 def date_clause(table, field, from, to, is_custom_filter)
1168 def date_clause(table, field, from, to, is_custom_filter)
1174 s = []
1169 s = []
1175 if from
1170 if from
1176 if from.is_a?(Date)
1171 if from.is_a?(Date)
1177 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1172 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1178 else
1173 else
1179 from = from - 1 # second
1174 from = from - 1 # second
1180 end
1175 end
1181 if self.class.default_timezone == :utc
1176 if self.class.default_timezone == :utc
1182 from = from.utc
1177 from = from.utc
1183 end
1178 end
1184 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1179 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1185 end
1180 end
1186 if to
1181 if to
1187 if to.is_a?(Date)
1182 if to.is_a?(Date)
1188 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1183 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1189 end
1184 end
1190 if self.class.default_timezone == :utc
1185 if self.class.default_timezone == :utc
1191 to = to.utc
1186 to = to.utc
1192 end
1187 end
1193 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1188 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1194 end
1189 end
1195 s.join(' AND ')
1190 s.join(' AND ')
1196 end
1191 end
1197
1192
1198 # Returns a SQL clause for a date or datetime field using relative dates.
1193 # Returns a SQL clause for a date or datetime field using relative dates.
1199 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1194 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1200 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1195 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1201 end
1196 end
1202
1197
1203 # Returns a Date or Time from the given filter value
1198 # Returns a Date or Time from the given filter value
1204 def parse_date(arg)
1199 def parse_date(arg)
1205 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1200 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1206 Time.parse(arg) rescue nil
1201 Time.parse(arg) rescue nil
1207 else
1202 else
1208 Date.parse(arg) rescue nil
1203 Date.parse(arg) rescue nil
1209 end
1204 end
1210 end
1205 end
1211
1206
1212 # Additional joins required for the given sort options
1207 # Additional joins required for the given sort options
1213 def joins_for_order_statement(order_options)
1208 def joins_for_order_statement(order_options)
1214 joins = []
1209 joins = []
1215
1210
1216 if order_options
1211 if order_options
1217 if order_options.include?('authors')
1212 if order_options.include?('authors')
1218 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1213 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1219 end
1214 end
1220 order_options.scan(/cf_\d+/).uniq.each do |name|
1215 order_options.scan(/cf_\d+/).uniq.each do |name|
1221 column = available_columns.detect {|c| c.name.to_s == name}
1216 column = available_columns.detect {|c| c.name.to_s == name}
1222 join = column && column.custom_field.join_for_order_statement
1217 join = column && column.custom_field.join_for_order_statement
1223 if join
1218 if join
1224 joins << join
1219 joins << join
1225 end
1220 end
1226 end
1221 end
1227 end
1222 end
1228
1223
1229 joins.any? ? joins.join(' ') : nil
1224 joins.any? ? joins.join(' ') : nil
1230 end
1225 end
1231 end
1226 end
@@ -1,979 +1,983
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 require 'uri'
18 require 'uri'
19
19
20 module Redmine
20 module Redmine
21 module FieldFormat
21 module FieldFormat
22 def self.add(name, klass)
22 def self.add(name, klass)
23 all[name.to_s] = klass.instance
23 all[name.to_s] = klass.instance
24 end
24 end
25
25
26 def self.delete(name)
26 def self.delete(name)
27 all.delete(name.to_s)
27 all.delete(name.to_s)
28 end
28 end
29
29
30 def self.all
30 def self.all
31 @formats ||= Hash.new(Base.instance)
31 @formats ||= Hash.new(Base.instance)
32 end
32 end
33
33
34 def self.available_formats
34 def self.available_formats
35 all.keys
35 all.keys
36 end
36 end
37
37
38 def self.find(name)
38 def self.find(name)
39 all[name.to_s]
39 all[name.to_s]
40 end
40 end
41
41
42 # Return an array of custom field formats which can be used in select_tag
42 # Return an array of custom field formats which can be used in select_tag
43 def self.as_select(class_name=nil)
43 def self.as_select(class_name=nil)
44 formats = all.values.select do |format|
44 formats = all.values.select do |format|
45 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
45 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
46 end
46 end
47 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
47 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
48 end
48 end
49
49
50 # Returns an array of formats that can be used for a custom field class
50 # Returns an array of formats that can be used for a custom field class
51 def self.formats_for_custom_field_class(klass=nil)
51 def self.formats_for_custom_field_class(klass=nil)
52 all.values.select do |format|
52 all.values.select do |format|
53 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(klass.name)
53 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(klass.name)
54 end
54 end
55 end
55 end
56
56
57 class Base
57 class Base
58 include Singleton
58 include Singleton
59 include Redmine::I18n
59 include Redmine::I18n
60 include Redmine::Helpers::URL
60 include Redmine::Helpers::URL
61 include ERB::Util
61 include ERB::Util
62
62
63 class_attribute :format_name
63 class_attribute :format_name
64 self.format_name = nil
64 self.format_name = nil
65
65
66 # Set this to true if the format supports multiple values
66 # Set this to true if the format supports multiple values
67 class_attribute :multiple_supported
67 class_attribute :multiple_supported
68 self.multiple_supported = false
68 self.multiple_supported = false
69
69
70 # Set this to true if the format supports filtering on custom values
70 # Set this to true if the format supports filtering on custom values
71 class_attribute :is_filter_supported
71 class_attribute :is_filter_supported
72 self.is_filter_supported = true
72 self.is_filter_supported = true
73
73
74 # Set this to true if the format supports textual search on custom values
74 # Set this to true if the format supports textual search on custom values
75 class_attribute :searchable_supported
75 class_attribute :searchable_supported
76 self.searchable_supported = false
76 self.searchable_supported = false
77
77
78 # Set this to true if field values can be summed up
78 # Set this to true if field values can be summed up
79 class_attribute :totalable_supported
79 class_attribute :totalable_supported
80 self.totalable_supported = false
80 self.totalable_supported = false
81
81
82 # Set this to false if field cannot be bulk edited
82 # Set this to false if field cannot be bulk edited
83 class_attribute :bulk_edit_supported
83 class_attribute :bulk_edit_supported
84 self.bulk_edit_supported = true
84 self.bulk_edit_supported = true
85
85
86 # Restricts the classes that the custom field can be added to
86 # Restricts the classes that the custom field can be added to
87 # Set to nil for no restrictions
87 # Set to nil for no restrictions
88 class_attribute :customized_class_names
88 class_attribute :customized_class_names
89 self.customized_class_names = nil
89 self.customized_class_names = nil
90
90
91 # Name of the partial for editing the custom field
91 # Name of the partial for editing the custom field
92 class_attribute :form_partial
92 class_attribute :form_partial
93 self.form_partial = nil
93 self.form_partial = nil
94
94
95 class_attribute :change_as_diff
95 class_attribute :change_as_diff
96 self.change_as_diff = false
96 self.change_as_diff = false
97
97
98 class_attribute :change_no_details
98 class_attribute :change_no_details
99 self.change_no_details = false
99 self.change_no_details = false
100
100
101 def self.add(name)
101 def self.add(name)
102 self.format_name = name
102 self.format_name = name
103 Redmine::FieldFormat.add(name, self)
103 Redmine::FieldFormat.add(name, self)
104 end
104 end
105 private_class_method :add
105 private_class_method :add
106
106
107 def self.field_attributes(*args)
107 def self.field_attributes(*args)
108 CustomField.store_accessor :format_store, *args
108 CustomField.store_accessor :format_store, *args
109 end
109 end
110
110
111 field_attributes :url_pattern
111 field_attributes :url_pattern
112
112
113 def name
113 def name
114 self.class.format_name
114 self.class.format_name
115 end
115 end
116
116
117 def label
117 def label
118 "label_#{name}"
118 "label_#{name}"
119 end
119 end
120
120
121 def set_custom_field_value(custom_field, custom_field_value, value)
121 def set_custom_field_value(custom_field, custom_field_value, value)
122 if value.is_a?(Array)
122 if value.is_a?(Array)
123 value = value.map(&:to_s).reject{|v| v==''}.uniq
123 value = value.map(&:to_s).reject{|v| v==''}.uniq
124 if value.empty?
124 if value.empty?
125 value << ''
125 value << ''
126 end
126 end
127 else
127 else
128 value = value.to_s
128 value = value.to_s
129 end
129 end
130
130
131 value
131 value
132 end
132 end
133
133
134 def cast_custom_value(custom_value)
134 def cast_custom_value(custom_value)
135 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
135 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
136 end
136 end
137
137
138 def cast_value(custom_field, value, customized=nil)
138 def cast_value(custom_field, value, customized=nil)
139 if value.blank?
139 if value.blank?
140 nil
140 nil
141 elsif value.is_a?(Array)
141 elsif value.is_a?(Array)
142 casted = value.map do |v|
142 casted = value.map do |v|
143 cast_single_value(custom_field, v, customized)
143 cast_single_value(custom_field, v, customized)
144 end
144 end
145 casted.compact.sort
145 casted.compact.sort
146 else
146 else
147 cast_single_value(custom_field, value, customized)
147 cast_single_value(custom_field, value, customized)
148 end
148 end
149 end
149 end
150
150
151 def cast_single_value(custom_field, value, customized=nil)
151 def cast_single_value(custom_field, value, customized=nil)
152 value.to_s
152 value.to_s
153 end
153 end
154
154
155 def target_class
155 def target_class
156 nil
156 nil
157 end
157 end
158
158
159 def possible_custom_value_options(custom_value)
159 def possible_custom_value_options(custom_value)
160 possible_values_options(custom_value.custom_field, custom_value.customized)
160 possible_values_options(custom_value.custom_field, custom_value.customized)
161 end
161 end
162
162
163 def possible_values_options(custom_field, object=nil)
163 def possible_values_options(custom_field, object=nil)
164 []
164 []
165 end
165 end
166
166
167 def value_from_keyword(custom_field, keyword, object)
167 def value_from_keyword(custom_field, keyword, object)
168 possible_values_options = possible_values_options(custom_field, object)
168 possible_values_options = possible_values_options(custom_field, object)
169 if possible_values_options.present?
169 if possible_values_options.present?
170 keyword = keyword.to_s
170 keyword = keyword.to_s
171 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
171 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
172 if v.is_a?(Array)
172 if v.is_a?(Array)
173 v.last
173 v.last
174 else
174 else
175 v
175 v
176 end
176 end
177 end
177 end
178 else
178 else
179 keyword
179 keyword
180 end
180 end
181 end
181 end
182
182
183 # Returns the validation errors for custom_field
183 # Returns the validation errors for custom_field
184 # Should return an empty array if custom_field is valid
184 # Should return an empty array if custom_field is valid
185 def validate_custom_field(custom_field)
185 def validate_custom_field(custom_field)
186 errors = []
186 errors = []
187 pattern = custom_field.url_pattern
187 pattern = custom_field.url_pattern
188 if pattern.present? && !uri_with_safe_scheme?(url_pattern_without_tokens(pattern))
188 if pattern.present? && !uri_with_safe_scheme?(url_pattern_without_tokens(pattern))
189 errors << [:url_pattern, :invalid]
189 errors << [:url_pattern, :invalid]
190 end
190 end
191 errors
191 errors
192 end
192 end
193
193
194 # Returns the validation error messages for custom_value
194 # Returns the validation error messages for custom_value
195 # Should return an empty array if custom_value is valid
195 # Should return an empty array if custom_value is valid
196 # custom_value is a CustomFieldValue.
196 # custom_value is a CustomFieldValue.
197 def validate_custom_value(custom_value)
197 def validate_custom_value(custom_value)
198 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
198 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
199 errors = values.map do |value|
199 errors = values.map do |value|
200 validate_single_value(custom_value.custom_field, value, custom_value.customized)
200 validate_single_value(custom_value.custom_field, value, custom_value.customized)
201 end
201 end
202 errors.flatten.uniq
202 errors.flatten.uniq
203 end
203 end
204
204
205 def validate_single_value(custom_field, value, customized=nil)
205 def validate_single_value(custom_field, value, customized=nil)
206 []
206 []
207 end
207 end
208
208
209 # CustomValue after_save callback
209 # CustomValue after_save callback
210 def after_save_custom_value(custom_field, custom_value)
210 def after_save_custom_value(custom_field, custom_value)
211 end
211 end
212
212
213 def formatted_custom_value(view, custom_value, html=false)
213 def formatted_custom_value(view, custom_value, html=false)
214 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
214 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
215 end
215 end
216
216
217 def formatted_value(view, custom_field, value, customized=nil, html=false)
217 def formatted_value(view, custom_field, value, customized=nil, html=false)
218 casted = cast_value(custom_field, value, customized)
218 casted = cast_value(custom_field, value, customized)
219 if html && custom_field.url_pattern.present?
219 if html && custom_field.url_pattern.present?
220 texts_and_urls = Array.wrap(casted).map do |single_value|
220 texts_and_urls = Array.wrap(casted).map do |single_value|
221 text = view.format_object(single_value, false).to_s
221 text = view.format_object(single_value, false).to_s
222 url = url_from_pattern(custom_field, single_value, customized)
222 url = url_from_pattern(custom_field, single_value, customized)
223 [text, url]
223 [text, url]
224 end
224 end
225 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to_if uri_with_safe_scheme?(url), text, url}
225 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to_if uri_with_safe_scheme?(url), text, url}
226 links.join(', ').html_safe
226 links.join(', ').html_safe
227 else
227 else
228 casted
228 casted
229 end
229 end
230 end
230 end
231
231
232 # Returns an URL generated with the custom field URL pattern
232 # Returns an URL generated with the custom field URL pattern
233 # and variables substitution:
233 # and variables substitution:
234 # %value% => the custom field value
234 # %value% => the custom field value
235 # %id% => id of the customized object
235 # %id% => id of the customized object
236 # %project_id% => id of the project of the customized object if defined
236 # %project_id% => id of the project of the customized object if defined
237 # %project_identifier% => identifier of the project of the customized object if defined
237 # %project_identifier% => identifier of the project of the customized object if defined
238 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
238 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
239 def url_from_pattern(custom_field, value, customized)
239 def url_from_pattern(custom_field, value, customized)
240 url = custom_field.url_pattern.to_s.dup
240 url = custom_field.url_pattern.to_s.dup
241 url.gsub!('%value%') {URI.encode value.to_s}
241 url.gsub!('%value%') {URI.encode value.to_s}
242 url.gsub!('%id%') {URI.encode customized.id.to_s}
242 url.gsub!('%id%') {URI.encode customized.id.to_s}
243 url.gsub!('%project_id%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
243 url.gsub!('%project_id%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
244 url.gsub!('%project_identifier%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
244 url.gsub!('%project_identifier%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
245 if custom_field.regexp.present?
245 if custom_field.regexp.present?
246 url.gsub!(%r{%m(\d+)%}) do
246 url.gsub!(%r{%m(\d+)%}) do
247 m = $1.to_i
247 m = $1.to_i
248 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
248 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
249 URI.encode matches[m].to_s
249 URI.encode matches[m].to_s
250 end
250 end
251 end
251 end
252 end
252 end
253 url
253 url
254 end
254 end
255 protected :url_from_pattern
255 protected :url_from_pattern
256
256
257 # Returns the URL pattern with substitution tokens removed,
257 # Returns the URL pattern with substitution tokens removed,
258 # for validation purpose
258 # for validation purpose
259 def url_pattern_without_tokens(url_pattern)
259 def url_pattern_without_tokens(url_pattern)
260 url_pattern.to_s.gsub(/%(value|id|project_id|project_identifier|m\d+)%/, '')
260 url_pattern.to_s.gsub(/%(value|id|project_id|project_identifier|m\d+)%/, '')
261 end
261 end
262 protected :url_pattern_without_tokens
262 protected :url_pattern_without_tokens
263
263
264 def edit_tag(view, tag_id, tag_name, custom_value, options={})
264 def edit_tag(view, tag_id, tag_name, custom_value, options={})
265 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
265 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
266 end
266 end
267
267
268 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
268 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
269 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
269 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
270 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
270 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
271 end
271 end
272
272
273 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
273 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
274 if custom_field.is_required?
274 if custom_field.is_required?
275 ''.html_safe
275 ''.html_safe
276 else
276 else
277 view.content_tag('label',
277 view.content_tag('label',
278 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
278 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
279 :class => 'inline'
279 :class => 'inline'
280 )
280 )
281 end
281 end
282 end
282 end
283 protected :bulk_clear_tag
283 protected :bulk_clear_tag
284
284
285 def query_filter_options(custom_field, query)
285 def query_filter_options(custom_field, query)
286 {:type => :string}
286 {:type => :string}
287 end
287 end
288
288
289 def before_custom_field_save(custom_field)
289 def before_custom_field_save(custom_field)
290 end
290 end
291
291
292 # Returns a ORDER BY clause that can used to sort customized
292 # Returns a ORDER BY clause that can used to sort customized
293 # objects by their value of the custom field.
293 # objects by their value of the custom field.
294 # Returns nil if the custom field can not be used for sorting.
294 # Returns nil if the custom field can not be used for sorting.
295 def order_statement(custom_field)
295 def order_statement(custom_field)
296 # COALESCE is here to make sure that blank and NULL values are sorted equally
296 # COALESCE is here to make sure that blank and NULL values are sorted equally
297 "COALESCE(#{join_alias custom_field}.value, '')"
297 "COALESCE(#{join_alias custom_field}.value, '')"
298 end
298 end
299
299
300 # Returns a GROUP BY clause that can used to group by custom value
300 # Returns a GROUP BY clause that can used to group by custom value
301 # Returns nil if the custom field can not be used for grouping.
301 # Returns nil if the custom field can not be used for grouping.
302 def group_statement(custom_field)
302 def group_statement(custom_field)
303 nil
303 nil
304 end
304 end
305
305
306 # Returns a JOIN clause that is added to the query when sorting by custom values
306 # Returns a JOIN clause that is added to the query when sorting by custom values
307 def join_for_order_statement(custom_field)
307 def join_for_order_statement(custom_field)
308 alias_name = join_alias(custom_field)
308 alias_name = join_alias(custom_field)
309
309
310 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
310 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
311 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
311 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
312 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
312 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
313 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
313 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
314 " AND (#{custom_field.visibility_by_project_condition})" +
314 " AND (#{custom_field.visibility_by_project_condition})" +
315 " AND #{alias_name}.value <> ''" +
315 " AND #{alias_name}.value <> ''" +
316 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
316 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
317 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
317 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
318 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
318 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
319 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
319 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
320 end
320 end
321
321
322 def join_alias(custom_field)
322 def join_alias(custom_field)
323 "cf_#{custom_field.id}"
323 "cf_#{custom_field.id}"
324 end
324 end
325 protected :join_alias
325 protected :join_alias
326 end
326 end
327
327
328 class Unbounded < Base
328 class Unbounded < Base
329 def validate_single_value(custom_field, value, customized=nil)
329 def validate_single_value(custom_field, value, customized=nil)
330 errs = super
330 errs = super
331 value = value.to_s
331 value = value.to_s
332 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
332 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
333 errs << ::I18n.t('activerecord.errors.messages.invalid')
333 errs << ::I18n.t('activerecord.errors.messages.invalid')
334 end
334 end
335 if custom_field.min_length && value.length < custom_field.min_length
335 if custom_field.min_length && value.length < custom_field.min_length
336 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
336 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
337 end
337 end
338 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
338 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
339 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
339 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
340 end
340 end
341 errs
341 errs
342 end
342 end
343 end
343 end
344
344
345 class StringFormat < Unbounded
345 class StringFormat < Unbounded
346 add 'string'
346 add 'string'
347 self.searchable_supported = true
347 self.searchable_supported = true
348 self.form_partial = 'custom_fields/formats/string'
348 self.form_partial = 'custom_fields/formats/string'
349 field_attributes :text_formatting
349 field_attributes :text_formatting
350
350
351 def formatted_value(view, custom_field, value, customized=nil, html=false)
351 def formatted_value(view, custom_field, value, customized=nil, html=false)
352 if html
352 if html
353 if custom_field.url_pattern.present?
353 if custom_field.url_pattern.present?
354 super
354 super
355 elsif custom_field.text_formatting == 'full'
355 elsif custom_field.text_formatting == 'full'
356 view.textilizable(value, :object => customized)
356 view.textilizable(value, :object => customized)
357 else
357 else
358 value.to_s
358 value.to_s
359 end
359 end
360 else
360 else
361 value.to_s
361 value.to_s
362 end
362 end
363 end
363 end
364 end
364 end
365
365
366 class TextFormat < Unbounded
366 class TextFormat < Unbounded
367 add 'text'
367 add 'text'
368 self.searchable_supported = true
368 self.searchable_supported = true
369 self.form_partial = 'custom_fields/formats/text'
369 self.form_partial = 'custom_fields/formats/text'
370 self.change_as_diff = true
370 self.change_as_diff = true
371
371
372 def formatted_value(view, custom_field, value, customized=nil, html=false)
372 def formatted_value(view, custom_field, value, customized=nil, html=false)
373 if html
373 if html
374 if value.present?
374 if value.present?
375 if custom_field.text_formatting == 'full'
375 if custom_field.text_formatting == 'full'
376 view.textilizable(value, :object => customized)
376 view.textilizable(value, :object => customized)
377 else
377 else
378 view.simple_format(html_escape(value))
378 view.simple_format(html_escape(value))
379 end
379 end
380 else
380 else
381 ''
381 ''
382 end
382 end
383 else
383 else
384 value.to_s
384 value.to_s
385 end
385 end
386 end
386 end
387
387
388 def edit_tag(view, tag_id, tag_name, custom_value, options={})
388 def edit_tag(view, tag_id, tag_name, custom_value, options={})
389 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
389 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
390 end
390 end
391
391
392 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
392 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
393 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
393 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
394 '<br />'.html_safe +
394 '<br />'.html_safe +
395 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
395 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
396 end
396 end
397
397
398 def query_filter_options(custom_field, query)
398 def query_filter_options(custom_field, query)
399 {:type => :text}
399 {:type => :text}
400 end
400 end
401 end
401 end
402
402
403 class LinkFormat < StringFormat
403 class LinkFormat < StringFormat
404 add 'link'
404 add 'link'
405 self.searchable_supported = false
405 self.searchable_supported = false
406 self.form_partial = 'custom_fields/formats/link'
406 self.form_partial = 'custom_fields/formats/link'
407
407
408 def formatted_value(view, custom_field, value, customized=nil, html=false)
408 def formatted_value(view, custom_field, value, customized=nil, html=false)
409 if html && value.present?
409 if html && value.present?
410 if custom_field.url_pattern.present?
410 if custom_field.url_pattern.present?
411 url = url_from_pattern(custom_field, value, customized)
411 url = url_from_pattern(custom_field, value, customized)
412 else
412 else
413 url = value.to_s
413 url = value.to_s
414 unless url =~ %r{\A[a-z]+://}i
414 unless url =~ %r{\A[a-z]+://}i
415 # no protocol found, use http by default
415 # no protocol found, use http by default
416 url = "http://" + url
416 url = "http://" + url
417 end
417 end
418 end
418 end
419 view.link_to value.to_s.truncate(40), url
419 view.link_to value.to_s.truncate(40), url
420 else
420 else
421 value.to_s
421 value.to_s
422 end
422 end
423 end
423 end
424 end
424 end
425
425
426 class Numeric < Unbounded
426 class Numeric < Unbounded
427 self.form_partial = 'custom_fields/formats/numeric'
427 self.form_partial = 'custom_fields/formats/numeric'
428 self.totalable_supported = true
428 self.totalable_supported = true
429
429
430 def order_statement(custom_field)
430 def order_statement(custom_field)
431 # Make the database cast values into numeric
431 # Make the database cast values into numeric
432 # Postgresql will raise an error if a value can not be casted!
432 # Postgresql will raise an error if a value can not be casted!
433 # CustomValue validations should ensure that it doesn't occur
433 # CustomValue validations should ensure that it doesn't occur
434 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
434 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
435 end
435 end
436
436
437 # Returns totals for the given scope
437 # Returns totals for the given scope
438 def total_for_scope(custom_field, scope)
438 def total_for_scope(custom_field, scope)
439 scope.joins(:custom_values).
439 scope.joins(:custom_values).
440 where(:custom_values => {:custom_field_id => custom_field.id}).
440 where(:custom_values => {:custom_field_id => custom_field.id}).
441 where.not(:custom_values => {:value => ''}).
441 where.not(:custom_values => {:value => ''}).
442 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
442 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
443 end
443 end
444
444
445 def cast_total_value(custom_field, value)
445 def cast_total_value(custom_field, value)
446 cast_single_value(custom_field, value)
446 cast_single_value(custom_field, value)
447 end
447 end
448 end
448 end
449
449
450 class IntFormat < Numeric
450 class IntFormat < Numeric
451 add 'int'
451 add 'int'
452
452
453 def label
453 def label
454 "label_integer"
454 "label_integer"
455 end
455 end
456
456
457 def cast_single_value(custom_field, value, customized=nil)
457 def cast_single_value(custom_field, value, customized=nil)
458 value.to_i
458 value.to_i
459 end
459 end
460
460
461 def validate_single_value(custom_field, value, customized=nil)
461 def validate_single_value(custom_field, value, customized=nil)
462 errs = super
462 errs = super
463 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
463 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
464 errs
464 errs
465 end
465 end
466
466
467 def query_filter_options(custom_field, query)
467 def query_filter_options(custom_field, query)
468 {:type => :integer}
468 {:type => :integer}
469 end
469 end
470
470
471 def group_statement(custom_field)
471 def group_statement(custom_field)
472 order_statement(custom_field)
472 order_statement(custom_field)
473 end
473 end
474 end
474 end
475
475
476 class FloatFormat < Numeric
476 class FloatFormat < Numeric
477 add 'float'
477 add 'float'
478
478
479 def cast_single_value(custom_field, value, customized=nil)
479 def cast_single_value(custom_field, value, customized=nil)
480 value.to_f
480 value.to_f
481 end
481 end
482
482
483 def cast_total_value(custom_field, value)
483 def cast_total_value(custom_field, value)
484 value.to_f.round(2)
484 value.to_f.round(2)
485 end
485 end
486
486
487 def validate_single_value(custom_field, value, customized=nil)
487 def validate_single_value(custom_field, value, customized=nil)
488 errs = super
488 errs = super
489 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
489 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
490 errs
490 errs
491 end
491 end
492
492
493 def query_filter_options(custom_field, query)
493 def query_filter_options(custom_field, query)
494 {:type => :float}
494 {:type => :float}
495 end
495 end
496 end
496 end
497
497
498 class DateFormat < Unbounded
498 class DateFormat < Unbounded
499 add 'date'
499 add 'date'
500 self.form_partial = 'custom_fields/formats/date'
500 self.form_partial = 'custom_fields/formats/date'
501
501
502 def cast_single_value(custom_field, value, customized=nil)
502 def cast_single_value(custom_field, value, customized=nil)
503 value.to_date rescue nil
503 value.to_date rescue nil
504 end
504 end
505
505
506 def validate_single_value(custom_field, value, customized=nil)
506 def validate_single_value(custom_field, value, customized=nil)
507 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
507 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
508 []
508 []
509 else
509 else
510 [::I18n.t('activerecord.errors.messages.not_a_date')]
510 [::I18n.t('activerecord.errors.messages.not_a_date')]
511 end
511 end
512 end
512 end
513
513
514 def edit_tag(view, tag_id, tag_name, custom_value, options={})
514 def edit_tag(view, tag_id, tag_name, custom_value, options={})
515 view.date_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
515 view.date_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
516 view.calendar_for(tag_id)
516 view.calendar_for(tag_id)
517 end
517 end
518
518
519 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
519 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
520 view.date_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
520 view.date_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
521 view.calendar_for(tag_id) +
521 view.calendar_for(tag_id) +
522 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
522 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
523 end
523 end
524
524
525 def query_filter_options(custom_field, query)
525 def query_filter_options(custom_field, query)
526 {:type => :date}
526 {:type => :date}
527 end
527 end
528
528
529 def group_statement(custom_field)
529 def group_statement(custom_field)
530 order_statement(custom_field)
530 order_statement(custom_field)
531 end
531 end
532 end
532 end
533
533
534 class List < Base
534 class List < Base
535 self.multiple_supported = true
535 self.multiple_supported = true
536 field_attributes :edit_tag_style
536 field_attributes :edit_tag_style
537
537
538 def edit_tag(view, tag_id, tag_name, custom_value, options={})
538 def edit_tag(view, tag_id, tag_name, custom_value, options={})
539 if custom_value.custom_field.edit_tag_style == 'check_box'
539 if custom_value.custom_field.edit_tag_style == 'check_box'
540 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
540 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
541 else
541 else
542 select_edit_tag(view, tag_id, tag_name, custom_value, options)
542 select_edit_tag(view, tag_id, tag_name, custom_value, options)
543 end
543 end
544 end
544 end
545
545
546 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
546 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
547 opts = []
547 opts = []
548 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
548 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
549 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
549 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
550 opts += possible_values_options(custom_field, objects)
550 opts += possible_values_options(custom_field, objects)
551 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
551 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
552 end
552 end
553
553
554 def query_filter_options(custom_field, query)
554 def query_filter_options(custom_field, query)
555 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
555 {:type => :list_optional, :values => lambda { query_filter_values(custom_field, query) }}
556 end
556 end
557
557
558 protected
558 protected
559
559
560 # Returns the values that are available in the field filter
560 # Returns the values that are available in the field filter
561 def query_filter_values(custom_field, query)
561 def query_filter_values(custom_field, query)
562 possible_values_options(custom_field, query.project)
562 possible_values_options(custom_field, query.project)
563 end
563 end
564
564
565 # Renders the edit tag as a select tag
565 # Renders the edit tag as a select tag
566 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
566 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
567 blank_option = ''.html_safe
567 blank_option = ''.html_safe
568 unless custom_value.custom_field.multiple?
568 unless custom_value.custom_field.multiple?
569 if custom_value.custom_field.is_required?
569 if custom_value.custom_field.is_required?
570 unless custom_value.custom_field.default_value.present?
570 unless custom_value.custom_field.default_value.present?
571 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
571 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
572 end
572 end
573 else
573 else
574 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
574 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
575 end
575 end
576 end
576 end
577 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
577 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
578 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
578 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
579 if custom_value.custom_field.multiple?
579 if custom_value.custom_field.multiple?
580 s << view.hidden_field_tag(tag_name, '')
580 s << view.hidden_field_tag(tag_name, '')
581 end
581 end
582 s
582 s
583 end
583 end
584
584
585 # Renders the edit tag as check box or radio tags
585 # Renders the edit tag as check box or radio tags
586 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
586 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
587 opts = []
587 opts = []
588 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
588 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
589 opts << ["(#{l(:label_none)})", '']
589 opts << ["(#{l(:label_none)})", '']
590 end
590 end
591 opts += possible_custom_value_options(custom_value)
591 opts += possible_custom_value_options(custom_value)
592 s = ''.html_safe
592 s = ''.html_safe
593 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
593 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
594 opts.each do |label, value|
594 opts.each do |label, value|
595 value ||= label
595 value ||= label
596 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
596 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
597 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
597 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
598 # set the id on the first tag only
598 # set the id on the first tag only
599 tag_id = nil
599 tag_id = nil
600 s << view.content_tag('label', tag + ' ' + label)
600 s << view.content_tag('label', tag + ' ' + label)
601 end
601 end
602 if custom_value.custom_field.multiple?
602 if custom_value.custom_field.multiple?
603 s << view.hidden_field_tag(tag_name, '')
603 s << view.hidden_field_tag(tag_name, '')
604 end
604 end
605 css = "#{options[:class]} check_box_group"
605 css = "#{options[:class]} check_box_group"
606 view.content_tag('span', s, options.merge(:class => css))
606 view.content_tag('span', s, options.merge(:class => css))
607 end
607 end
608 end
608 end
609
609
610 class ListFormat < List
610 class ListFormat < List
611 add 'list'
611 add 'list'
612 self.searchable_supported = true
612 self.searchable_supported = true
613 self.form_partial = 'custom_fields/formats/list'
613 self.form_partial = 'custom_fields/formats/list'
614
614
615 def possible_custom_value_options(custom_value)
615 def possible_custom_value_options(custom_value)
616 options = possible_values_options(custom_value.custom_field)
616 options = possible_values_options(custom_value.custom_field)
617 missing = [custom_value.value].flatten.reject(&:blank?) - options
617 missing = [custom_value.value].flatten.reject(&:blank?) - options
618 if missing.any?
618 if missing.any?
619 options += missing
619 options += missing
620 end
620 end
621 options
621 options
622 end
622 end
623
623
624 def possible_values_options(custom_field, object=nil)
624 def possible_values_options(custom_field, object=nil)
625 custom_field.possible_values
625 custom_field.possible_values
626 end
626 end
627
627
628 def validate_custom_field(custom_field)
628 def validate_custom_field(custom_field)
629 errors = []
629 errors = []
630 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
630 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
631 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
631 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
632 errors
632 errors
633 end
633 end
634
634
635 def validate_custom_value(custom_value)
635 def validate_custom_value(custom_value)
636 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
636 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
637 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
637 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
638 if invalid_values.any?
638 if invalid_values.any?
639 [::I18n.t('activerecord.errors.messages.inclusion')]
639 [::I18n.t('activerecord.errors.messages.inclusion')]
640 else
640 else
641 []
641 []
642 end
642 end
643 end
643 end
644
644
645 def group_statement(custom_field)
645 def group_statement(custom_field)
646 order_statement(custom_field)
646 order_statement(custom_field)
647 end
647 end
648 end
648 end
649
649
650 class BoolFormat < List
650 class BoolFormat < List
651 add 'bool'
651 add 'bool'
652 self.multiple_supported = false
652 self.multiple_supported = false
653 self.form_partial = 'custom_fields/formats/bool'
653 self.form_partial = 'custom_fields/formats/bool'
654
654
655 def label
655 def label
656 "label_boolean"
656 "label_boolean"
657 end
657 end
658
658
659 def cast_single_value(custom_field, value, customized=nil)
659 def cast_single_value(custom_field, value, customized=nil)
660 value == '1' ? true : false
660 value == '1' ? true : false
661 end
661 end
662
662
663 def possible_values_options(custom_field, object=nil)
663 def possible_values_options(custom_field, object=nil)
664 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
664 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
665 end
665 end
666
666
667 def group_statement(custom_field)
667 def group_statement(custom_field)
668 order_statement(custom_field)
668 order_statement(custom_field)
669 end
669 end
670
670
671 def edit_tag(view, tag_id, tag_name, custom_value, options={})
671 def edit_tag(view, tag_id, tag_name, custom_value, options={})
672 case custom_value.custom_field.edit_tag_style
672 case custom_value.custom_field.edit_tag_style
673 when 'check_box'
673 when 'check_box'
674 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
674 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
675 when 'radio'
675 when 'radio'
676 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
676 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
677 else
677 else
678 select_edit_tag(view, tag_id, tag_name, custom_value, options)
678 select_edit_tag(view, tag_id, tag_name, custom_value, options)
679 end
679 end
680 end
680 end
681
681
682 # Renders the edit tag as a simple check box
682 # Renders the edit tag as a simple check box
683 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
683 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
684 s = ''.html_safe
684 s = ''.html_safe
685 s << view.hidden_field_tag(tag_name, '0', :id => nil)
685 s << view.hidden_field_tag(tag_name, '0', :id => nil)
686 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
686 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
687 view.content_tag('span', s, options)
687 view.content_tag('span', s, options)
688 end
688 end
689 end
689 end
690
690
691 class RecordList < List
691 class RecordList < List
692 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
692 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
693
693
694 def cast_single_value(custom_field, value, customized=nil)
694 def cast_single_value(custom_field, value, customized=nil)
695 target_class.find_by_id(value.to_i) if value.present?
695 target_class.find_by_id(value.to_i) if value.present?
696 end
696 end
697
697
698 def target_class
698 def target_class
699 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
699 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
700 end
700 end
701
701
702 def reset_target_class
702 def reset_target_class
703 @target_class = nil
703 @target_class = nil
704 end
704 end
705
705
706 def possible_custom_value_options(custom_value)
706 def possible_custom_value_options(custom_value)
707 options = possible_values_options(custom_value.custom_field, custom_value.customized)
707 options = possible_values_options(custom_value.custom_field, custom_value.customized)
708 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
708 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
709 if missing.any?
709 if missing.any?
710 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
710 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
711 end
711 end
712 options
712 options
713 end
713 end
714
714
715 def order_statement(custom_field)
715 def order_statement(custom_field)
716 if target_class.respond_to?(:fields_for_order_statement)
716 if target_class.respond_to?(:fields_for_order_statement)
717 target_class.fields_for_order_statement(value_join_alias(custom_field))
717 target_class.fields_for_order_statement(value_join_alias(custom_field))
718 end
718 end
719 end
719 end
720
720
721 def group_statement(custom_field)
721 def group_statement(custom_field)
722 "COALESCE(#{join_alias custom_field}.value, '')"
722 "COALESCE(#{join_alias custom_field}.value, '')"
723 end
723 end
724
724
725 def join_for_order_statement(custom_field)
725 def join_for_order_statement(custom_field)
726 alias_name = join_alias(custom_field)
726 alias_name = join_alias(custom_field)
727
727
728 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
728 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
729 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
729 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
730 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
730 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
731 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
731 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
732 " AND (#{custom_field.visibility_by_project_condition})" +
732 " AND (#{custom_field.visibility_by_project_condition})" +
733 " AND #{alias_name}.value <> ''" +
733 " AND #{alias_name}.value <> ''" +
734 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
734 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
735 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
735 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
736 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
736 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
737 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
737 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
738 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
738 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
739 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
739 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
740 end
740 end
741
741
742 def value_join_alias(custom_field)
742 def value_join_alias(custom_field)
743 join_alias(custom_field) + "_" + custom_field.field_format
743 join_alias(custom_field) + "_" + custom_field.field_format
744 end
744 end
745 protected :value_join_alias
745 protected :value_join_alias
746 end
746 end
747
747
748 class EnumerationFormat < RecordList
748 class EnumerationFormat < RecordList
749 add 'enumeration'
749 add 'enumeration'
750 self.form_partial = 'custom_fields/formats/enumeration'
750 self.form_partial = 'custom_fields/formats/enumeration'
751
751
752 def label
752 def label
753 "label_field_format_enumeration"
753 "label_field_format_enumeration"
754 end
754 end
755
755
756 def target_class
756 def target_class
757 @target_class ||= CustomFieldEnumeration
757 @target_class ||= CustomFieldEnumeration
758 end
758 end
759
759
760 def possible_values_options(custom_field, object=nil)
760 def possible_values_options(custom_field, object=nil)
761 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
761 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
762 end
762 end
763
763
764 def possible_values_records(custom_field, object=nil)
764 def possible_values_records(custom_field, object=nil)
765 custom_field.enumerations.active
765 custom_field.enumerations.active
766 end
766 end
767
767
768 def value_from_keyword(custom_field, keyword, object)
768 def value_from_keyword(custom_field, keyword, object)
769 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword).first
769 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword).first
770 value ? value.id : nil
770 value ? value.id : nil
771 end
771 end
772 end
772 end
773
773
774 class UserFormat < RecordList
774 class UserFormat < RecordList
775 add 'user'
775 add 'user'
776 self.form_partial = 'custom_fields/formats/user'
776 self.form_partial = 'custom_fields/formats/user'
777 field_attributes :user_role
777 field_attributes :user_role
778
778
779 def possible_values_options(custom_field, object=nil)
779 def possible_values_options(custom_field, object=nil)
780 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
780 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
781 end
781 end
782
782
783 def possible_values_records(custom_field, object=nil)
783 def possible_values_records(custom_field, object=nil)
784 if object.is_a?(Array)
784 if object.is_a?(Array)
785 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
785 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
786 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
786 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
787 elsif object.respond_to?(:project) && object.project
787 elsif object.respond_to?(:project) && object.project
788 scope = object.project.users
788 scope = object.project.users
789 if custom_field.user_role.is_a?(Array)
789 if custom_field.user_role.is_a?(Array)
790 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
790 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
791 if role_ids.any?
791 if role_ids.any?
792 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
792 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
793 end
793 end
794 end
794 end
795 scope.sorted
795 scope.sorted
796 else
796 else
797 []
797 []
798 end
798 end
799 end
799 end
800
800
801 def value_from_keyword(custom_field, keyword, object)
801 def value_from_keyword(custom_field, keyword, object)
802 users = possible_values_records(custom_field, object).to_a
802 users = possible_values_records(custom_field, object).to_a
803 user = Principal.detect_by_keyword(users, keyword)
803 user = Principal.detect_by_keyword(users, keyword)
804 user ? user.id : nil
804 user ? user.id : nil
805 end
805 end
806
806
807 def before_custom_field_save(custom_field)
807 def before_custom_field_save(custom_field)
808 super
808 super
809 if custom_field.user_role.is_a?(Array)
809 if custom_field.user_role.is_a?(Array)
810 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
810 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
811 end
811 end
812 end
812 end
813
814 def query_filter_values(*args)
815 [["<< #{l(:label_me)} >>", "me"]] + super
816 end
813 end
817 end
814
818
815 class VersionFormat < RecordList
819 class VersionFormat < RecordList
816 add 'version'
820 add 'version'
817 self.form_partial = 'custom_fields/formats/version'
821 self.form_partial = 'custom_fields/formats/version'
818 field_attributes :version_status
822 field_attributes :version_status
819
823
820 def possible_values_options(custom_field, object=nil)
824 def possible_values_options(custom_field, object=nil)
821 versions_options(custom_field, object)
825 versions_options(custom_field, object)
822 end
826 end
823
827
824 def before_custom_field_save(custom_field)
828 def before_custom_field_save(custom_field)
825 super
829 super
826 if custom_field.version_status.is_a?(Array)
830 if custom_field.version_status.is_a?(Array)
827 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
831 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
828 end
832 end
829 end
833 end
830
834
831 protected
835 protected
832
836
833 def query_filter_values(custom_field, query)
837 def query_filter_values(custom_field, query)
834 versions_options(custom_field, query.project, true)
838 versions_options(custom_field, query.project, true)
835 end
839 end
836
840
837 def versions_options(custom_field, object, all_statuses=false)
841 def versions_options(custom_field, object, all_statuses=false)
838 if object.is_a?(Array)
842 if object.is_a?(Array)
839 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
843 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
840 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
844 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
841 elsif object.respond_to?(:project) && object.project
845 elsif object.respond_to?(:project) && object.project
842 scope = object.project.shared_versions
846 scope = object.project.shared_versions
843 filtered_versions_options(custom_field, scope, all_statuses)
847 filtered_versions_options(custom_field, scope, all_statuses)
844 elsif object.nil?
848 elsif object.nil?
845 scope = ::Version.visible.where(:sharing => 'system')
849 scope = ::Version.visible.where(:sharing => 'system')
846 filtered_versions_options(custom_field, scope, all_statuses)
850 filtered_versions_options(custom_field, scope, all_statuses)
847 else
851 else
848 []
852 []
849 end
853 end
850 end
854 end
851
855
852 def filtered_versions_options(custom_field, scope, all_statuses=false)
856 def filtered_versions_options(custom_field, scope, all_statuses=false)
853 if !all_statuses && custom_field.version_status.is_a?(Array)
857 if !all_statuses && custom_field.version_status.is_a?(Array)
854 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
858 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
855 if statuses.any?
859 if statuses.any?
856 scope = scope.where(:status => statuses.map(&:to_s))
860 scope = scope.where(:status => statuses.map(&:to_s))
857 end
861 end
858 end
862 end
859 scope.sort.collect{|u| [u.to_s, u.id.to_s] }
863 scope.sort.collect{|u| [u.to_s, u.id.to_s] }
860 end
864 end
861 end
865 end
862
866
863 class AttachmentFormat < Base
867 class AttachmentFormat < Base
864 add 'attachment'
868 add 'attachment'
865 self.form_partial = 'custom_fields/formats/attachment'
869 self.form_partial = 'custom_fields/formats/attachment'
866 self.is_filter_supported = false
870 self.is_filter_supported = false
867 self.change_no_details = true
871 self.change_no_details = true
868 self.bulk_edit_supported = false
872 self.bulk_edit_supported = false
869 field_attributes :extensions_allowed
873 field_attributes :extensions_allowed
870
874
871 def set_custom_field_value(custom_field, custom_field_value, value)
875 def set_custom_field_value(custom_field, custom_field_value, value)
872 attachment_present = false
876 attachment_present = false
873
877
874 if value.is_a?(Hash)
878 if value.is_a?(Hash)
875 attachment_present = true
879 attachment_present = true
876 value = value.except(:blank)
880 value = value.except(:blank)
877
881
878 if value.values.any? && value.values.all? {|v| v.is_a?(Hash)}
882 if value.values.any? && value.values.all? {|v| v.is_a?(Hash)}
879 value = value.values.first
883 value = value.values.first
880 end
884 end
881
885
882 if value.key?(:id)
886 if value.key?(:id)
883 value = set_custom_field_value_by_id(custom_field, custom_field_value, value[:id])
887 value = set_custom_field_value_by_id(custom_field, custom_field_value, value[:id])
884 elsif value[:token].present?
888 elsif value[:token].present?
885 if attachment = Attachment.find_by_token(value[:token])
889 if attachment = Attachment.find_by_token(value[:token])
886 value = attachment.id.to_s
890 value = attachment.id.to_s
887 else
891 else
888 value = ''
892 value = ''
889 end
893 end
890 elsif value.key?(:file)
894 elsif value.key?(:file)
891 attachment = Attachment.new(:file => value[:file], :author => User.current)
895 attachment = Attachment.new(:file => value[:file], :author => User.current)
892 if attachment.save
896 if attachment.save
893 value = attachment.id.to_s
897 value = attachment.id.to_s
894 else
898 else
895 value = ''
899 value = ''
896 end
900 end
897 else
901 else
898 attachment_present = false
902 attachment_present = false
899 value = ''
903 value = ''
900 end
904 end
901 elsif value.is_a?(String)
905 elsif value.is_a?(String)
902 value = set_custom_field_value_by_id(custom_field, custom_field_value, value)
906 value = set_custom_field_value_by_id(custom_field, custom_field_value, value)
903 end
907 end
904 custom_field_value.instance_variable_set "@attachment_present", attachment_present
908 custom_field_value.instance_variable_set "@attachment_present", attachment_present
905
909
906 value
910 value
907 end
911 end
908
912
909 def set_custom_field_value_by_id(custom_field, custom_field_value, id)
913 def set_custom_field_value_by_id(custom_field, custom_field_value, id)
910 attachment = Attachment.find_by_id(id)
914 attachment = Attachment.find_by_id(id)
911 if attachment && attachment.container.is_a?(CustomValue) && attachment.container.customized == custom_field_value.customized
915 if attachment && attachment.container.is_a?(CustomValue) && attachment.container.customized == custom_field_value.customized
912 id.to_s
916 id.to_s
913 else
917 else
914 ''
918 ''
915 end
919 end
916 end
920 end
917 private :set_custom_field_value_by_id
921 private :set_custom_field_value_by_id
918
922
919 def cast_single_value(custom_field, value, customized=nil)
923 def cast_single_value(custom_field, value, customized=nil)
920 Attachment.find_by_id(value.to_i) if value.present? && value.respond_to?(:to_i)
924 Attachment.find_by_id(value.to_i) if value.present? && value.respond_to?(:to_i)
921 end
925 end
922
926
923 def validate_custom_value(custom_value)
927 def validate_custom_value(custom_value)
924 errors = []
928 errors = []
925
929
926 if custom_value.value.blank?
930 if custom_value.value.blank?
927 if custom_value.instance_variable_get("@attachment_present")
931 if custom_value.instance_variable_get("@attachment_present")
928 errors << ::I18n.t('activerecord.errors.messages.invalid')
932 errors << ::I18n.t('activerecord.errors.messages.invalid')
929 end
933 end
930 else
934 else
931 if custom_value.value.present?
935 if custom_value.value.present?
932 attachment = Attachment.where(:id => custom_value.value.to_s).first
936 attachment = Attachment.where(:id => custom_value.value.to_s).first
933 extensions = custom_value.custom_field.extensions_allowed
937 extensions = custom_value.custom_field.extensions_allowed
934 if attachment && extensions.present? && !attachment.extension_in?(extensions)
938 if attachment && extensions.present? && !attachment.extension_in?(extensions)
935 errors << "#{::I18n.t('activerecord.errors.messages.invalid')} (#{l(:setting_attachment_extensions_allowed)}: #{extensions})"
939 errors << "#{::I18n.t('activerecord.errors.messages.invalid')} (#{l(:setting_attachment_extensions_allowed)}: #{extensions})"
936 end
940 end
937 end
941 end
938 end
942 end
939
943
940 errors.uniq
944 errors.uniq
941 end
945 end
942
946
943 def after_save_custom_value(custom_field, custom_value)
947 def after_save_custom_value(custom_field, custom_value)
944 if custom_value.value_changed?
948 if custom_value.value_changed?
945 if custom_value.value.present?
949 if custom_value.value.present?
946 attachment = Attachment.where(:id => custom_value.value.to_s).first
950 attachment = Attachment.where(:id => custom_value.value.to_s).first
947 if attachment
951 if attachment
948 attachment.container = custom_value
952 attachment.container = custom_value
949 attachment.save!
953 attachment.save!
950 end
954 end
951 end
955 end
952 if custom_value.value_was.present?
956 if custom_value.value_was.present?
953 attachment = Attachment.where(:id => custom_value.value_was.to_s).first
957 attachment = Attachment.where(:id => custom_value.value_was.to_s).first
954 if attachment
958 if attachment
955 attachment.destroy
959 attachment.destroy
956 end
960 end
957 end
961 end
958 end
962 end
959 end
963 end
960
964
961 def edit_tag(view, tag_id, tag_name, custom_value, options={})
965 def edit_tag(view, tag_id, tag_name, custom_value, options={})
962 attachment = nil
966 attachment = nil
963 if custom_value.value.present?
967 if custom_value.value.present?
964 attachment = Attachment.find_by_id(custom_value.value)
968 attachment = Attachment.find_by_id(custom_value.value)
965 end
969 end
966
970
967 view.hidden_field_tag("#{tag_name}[blank]", "") +
971 view.hidden_field_tag("#{tag_name}[blank]", "") +
968 view.render(:partial => 'attachments/form',
972 view.render(:partial => 'attachments/form',
969 :locals => {
973 :locals => {
970 :attachment_param => tag_name,
974 :attachment_param => tag_name,
971 :multiple => false,
975 :multiple => false,
972 :description => false,
976 :description => false,
973 :saved_attachments => [attachment].compact,
977 :saved_attachments => [attachment].compact,
974 :filedrop => false
978 :filedrop => false
975 })
979 })
976 end
980 end
977 end
981 end
978 end
982 end
979 end
983 end
@@ -1,88 +1,88
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 require File.expand_path('../../../../../test_helper', __FILE__)
18 require File.expand_path('../../../../../test_helper', __FILE__)
19 require 'redmine/field_format'
19 require 'redmine/field_format'
20
20
21 class Redmine::VersionFieldFormatTest < ActionView::TestCase
21 class Redmine::VersionFieldFormatTest < ActionView::TestCase
22 fixtures :projects, :versions, :trackers,
22 fixtures :projects, :versions, :trackers,
23 :roles, :users, :members, :member_roles,
23 :roles, :users, :members, :member_roles,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :enumerations
25 :enumerations
26
26
27 def test_version_status_should_reject_blank_values
27 def test_version_status_should_reject_blank_values
28 field = IssueCustomField.new(:name => 'Foo', :field_format => 'version', :version_status => ["open", ""])
28 field = IssueCustomField.new(:name => 'Foo', :field_format => 'version', :version_status => ["open", ""])
29 field.save!
29 field.save!
30 assert_equal ["open"], field.version_status
30 assert_equal ["open"], field.version_status
31 end
31 end
32
32
33 def test_existing_values_should_be_valid
33 def test_existing_values_should_be_valid
34 field = IssueCustomField.create!(:name => 'Foo', :field_format => 'version', :is_for_all => true, :trackers => Tracker.all)
34 field = IssueCustomField.create!(:name => 'Foo', :field_format => 'version', :is_for_all => true, :trackers => Tracker.all)
35 project = Project.generate!
35 project = Project.generate!
36 version = Version.generate!(:project => project, :status => 'open')
36 version = Version.generate!(:project => project, :status => 'open')
37 issue = Issue.generate!(:project_id => project.id, :tracker_id => 1, :custom_field_values => {field.id => version.id})
37 issue = Issue.generate!(:project_id => project.id, :tracker_id => 1, :custom_field_values => {field.id => version.id})
38
38
39 field.version_status = ["open"]
39 field.version_status = ["open"]
40 field.save!
40 field.save!
41
41
42 issue = Issue.order('id DESC').first
42 issue = Issue.order('id DESC').first
43 assert_include [version.name, version.id.to_s], field.possible_custom_value_options(issue.custom_value_for(field))
43 assert_include [version.name, version.id.to_s], field.possible_custom_value_options(issue.custom_value_for(field))
44 assert issue.valid?
44 assert issue.valid?
45 end
45 end
46
46
47 def test_possible_values_options_should_return_project_versions
47 def test_possible_values_options_should_return_project_versions
48 field = IssueCustomField.new(:field_format => 'version')
48 field = IssueCustomField.new(:field_format => 'version')
49 project = Project.find(1)
49 project = Project.find(1)
50 expected = project.shared_versions.sort.map(&:name)
50 expected = project.shared_versions.sort.map(&:name)
51
51
52 assert_equal expected, field.possible_values_options(project).map(&:first)
52 assert_equal expected, field.possible_values_options(project).map(&:first)
53 end
53 end
54
54
55 def test_possible_values_options_should_return_system_shared_versions_without_project
55 def test_possible_values_options_should_return_system_shared_versions_without_project
56 field = IssueCustomField.new(:field_format => 'version')
56 field = IssueCustomField.new(:field_format => 'version')
57 version = Version.generate!(:project => Project.find(1), :status => 'open', :sharing => 'system')
57 version = Version.generate!(:project => Project.find(1), :status => 'open', :sharing => 'system')
58
58
59 expected = Version.visible.where(:sharing => 'system').sort.map(&:name)
59 expected = Version.visible.where(:sharing => 'system').sort.map(&:name)
60 assert_include version.name, expected
60 assert_include version.name, expected
61 assert_equal expected, field.possible_values_options.map(&:first)
61 assert_equal expected, field.possible_values_options.map(&:first)
62 end
62 end
63
63
64 def test_possible_values_options_should_return_project_versions_with_selected_status
64 def test_possible_values_options_should_return_project_versions_with_selected_status
65 field = IssueCustomField.new(:field_format => 'version', :version_status => ["open"])
65 field = IssueCustomField.new(:field_format => 'version', :version_status => ["open"])
66 project = Project.find(1)
66 project = Project.find(1)
67 expected = project.shared_versions.sort.select {|v| v.status == "open"}.map(&:name)
67 expected = project.shared_versions.sort.select {|v| v.status == "open"}.map(&:name)
68
68
69 assert_equal expected, field.possible_values_options(project).map(&:first)
69 assert_equal expected, field.possible_values_options(project).map(&:first)
70 end
70 end
71
71
72 def test_cast_value_should_not_raise_error_when_array_contains_value_casted_to_nil
72 def test_cast_value_should_not_raise_error_when_array_contains_value_casted_to_nil
73 field = IssueCustomField.new(:field_format => 'version')
73 field = IssueCustomField.new(:field_format => 'version')
74 assert_nothing_raised do
74 assert_nothing_raised do
75 field.cast_value([1,2, 42])
75 field.cast_value([1,2, 42])
76 end
76 end
77 end
77 end
78
78
79 def test_query_filter_options_should_include_versions_with_any_status
79 def test_query_filter_options_should_include_versions_with_any_status
80 field = IssueCustomField.new(:field_format => 'version', :version_status => ["open"])
80 field = IssueCustomField.new(:field_format => 'version', :version_status => ["open"])
81 project = Project.find(1)
81 project = Project.find(1)
82 version = Version.generate!(:project => project, :status => 'locked')
82 version = Version.generate!(:project => project, :status => 'locked')
83 query = Query.new(:project => project)
83 query = Query.new(:project => project)
84
84
85 assert_not_include version.name, field.possible_values_options(project).map(&:first)
85 assert_not_include version.name, field.possible_values_options(project).map(&:first)
86 assert_include version.name, field.query_filter_options(query)[:values].map(&:first)
86 assert_include version.name, field.query_filter_options(query)[:values].call.map(&:first)
87 end
87 end
88 end
88 end
General Comments 0
You need to be logged in to leave comments. Login now