##// END OF EJS Templates
Merged r14944 (#21413)....
Jean-Philippe Lang -
r14563:f2fd7905557d
parent child
Show More
@@ -1,280 +1,284
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 include Redmine::SubclassFactory
19 include Redmine::SubclassFactory
20
20
21 has_many :enumerations,
21 has_many :enumerations,
22 lambda { order(:position) },
22 lambda { order(:position) },
23 :class_name => 'CustomFieldEnumeration',
23 :class_name => 'CustomFieldEnumeration',
24 :dependent => :delete_all
24 :dependent => :delete_all
25 has_many :custom_values, :dependent => :delete_all
25 has_many :custom_values, :dependent => :delete_all
26 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
26 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
27 acts_as_list :scope => 'type = \'#{self.class}\''
27 acts_as_list :scope => 'type = \'#{self.class}\''
28 serialize :possible_values
28 serialize :possible_values
29 store :format_store
29 store :format_store
30
30
31 validates_presence_of :name, :field_format
31 validates_presence_of :name, :field_format
32 validates_uniqueness_of :name, :scope => :type
32 validates_uniqueness_of :name, :scope => :type
33 validates_length_of :name, :maximum => 30
33 validates_length_of :name, :maximum => 30
34 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
34 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
35 validate :validate_custom_field
35 validate :validate_custom_field
36 attr_protected :id
36 attr_protected :id
37
37
38 before_validation :set_searchable
38 before_validation :set_searchable
39 before_save do |field|
39 before_save do |field|
40 field.format.before_custom_field_save(field)
40 field.format.before_custom_field_save(field)
41 end
41 end
42 after_save :handle_multiplicity_change
42 after_save :handle_multiplicity_change
43 after_save do |field|
43 after_save do |field|
44 if field.visible_changed? && field.visible
44 if field.visible_changed? && field.visible
45 field.roles.clear
45 field.roles.clear
46 end
46 end
47 end
47 end
48
48
49 scope :sorted, lambda { order(:position) }
49 scope :sorted, lambda { order(:position) }
50 scope :visible, lambda {|*args|
50 scope :visible, lambda {|*args|
51 user = args.shift || User.current
51 user = args.shift || User.current
52 if user.admin?
52 if user.admin?
53 # nop
53 # nop
54 elsif user.memberships.any?
54 elsif user.memberships.any?
55 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
55 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
56 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
56 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
57 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
57 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
58 " WHERE m.user_id = ?)",
58 " WHERE m.user_id = ?)",
59 true, user.id)
59 true, user.id)
60 else
60 else
61 where(:visible => true)
61 where(:visible => true)
62 end
62 end
63 }
63 }
64
64
65 def visible_by?(project, user=User.current)
65 def visible_by?(project, user=User.current)
66 visible? || user.admin?
66 visible? || user.admin?
67 end
67 end
68
68
69 def format
69 def format
70 @format ||= Redmine::FieldFormat.find(field_format)
70 @format ||= Redmine::FieldFormat.find(field_format)
71 end
71 end
72
72
73 def field_format=(arg)
73 def field_format=(arg)
74 # cannot change format of a saved custom field
74 # cannot change format of a saved custom field
75 if new_record?
75 if new_record?
76 @format = nil
76 @format = nil
77 super
77 super
78 end
78 end
79 end
79 end
80
80
81 def set_searchable
81 def set_searchable
82 # make sure these fields are not searchable
82 # make sure these fields are not searchable
83 self.searchable = false unless format.class.searchable_supported
83 self.searchable = false unless format.class.searchable_supported
84 # make sure only these fields can have multiple values
84 # make sure only these fields can have multiple values
85 self.multiple = false unless format.class.multiple_supported
85 self.multiple = false unless format.class.multiple_supported
86 true
86 true
87 end
87 end
88
88
89 def validate_custom_field
89 def validate_custom_field
90 format.validate_custom_field(self).each do |attribute, message|
90 format.validate_custom_field(self).each do |attribute, message|
91 errors.add attribute, message
91 errors.add attribute, message
92 end
92 end
93
93
94 if regexp.present?
94 if regexp.present?
95 begin
95 begin
96 Regexp.new(regexp)
96 Regexp.new(regexp)
97 rescue
97 rescue
98 errors.add(:regexp, :invalid)
98 errors.add(:regexp, :invalid)
99 end
99 end
100 end
100 end
101
101
102 if default_value.present?
102 if default_value.present?
103 validate_field_value(default_value).each do |message|
103 validate_field_value(default_value).each do |message|
104 errors.add :default_value, message
104 errors.add :default_value, message
105 end
105 end
106 end
106 end
107 end
107 end
108
108
109 def possible_custom_value_options(custom_value)
109 def possible_custom_value_options(custom_value)
110 format.possible_custom_value_options(custom_value)
110 format.possible_custom_value_options(custom_value)
111 end
111 end
112
112
113 def possible_values_options(object=nil)
113 def possible_values_options(object=nil)
114 if object.is_a?(Array)
114 if object.is_a?(Array)
115 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
115 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
116 else
116 else
117 format.possible_values_options(self, object) || []
117 format.possible_values_options(self, object) || []
118 end
118 end
119 end
119 end
120
120
121 def possible_values
121 def possible_values
122 values = read_attribute(:possible_values)
122 values = read_attribute(:possible_values)
123 if values.is_a?(Array)
123 if values.is_a?(Array)
124 values.each do |value|
124 values.each do |value|
125 value.to_s.force_encoding('UTF-8')
125 value.to_s.force_encoding('UTF-8')
126 end
126 end
127 values
127 values
128 else
128 else
129 []
129 []
130 end
130 end
131 end
131 end
132
132
133 # Makes possible_values accept a multiline string
133 # Makes possible_values accept a multiline string
134 def possible_values=(arg)
134 def possible_values=(arg)
135 if arg.is_a?(Array)
135 if arg.is_a?(Array)
136 values = arg.compact.map {|a| a.to_s.strip}.reject(&:blank?)
136 values = arg.compact.map {|a| a.to_s.strip}.reject(&:blank?)
137 write_attribute(:possible_values, values)
137 write_attribute(:possible_values, values)
138 else
138 else
139 self.possible_values = arg.to_s.split(/[\n\r]+/)
139 self.possible_values = arg.to_s.split(/[\n\r]+/)
140 end
140 end
141 end
141 end
142
142
143 def cast_value(value)
143 def cast_value(value)
144 format.cast_value(self, value)
144 format.cast_value(self, value)
145 end
145 end
146
146
147 def value_from_keyword(keyword, customized)
147 def value_from_keyword(keyword, customized)
148 format.value_from_keyword(self, keyword, customized)
148 format.value_from_keyword(self, keyword, customized)
149 end
149 end
150
150
151 # Returns the options hash used to build a query filter for the field
151 # Returns the options hash used to build a query filter for the field
152 def query_filter_options(query)
152 def query_filter_options(query)
153 format.query_filter_options(self, query)
153 format.query_filter_options(self, query)
154 end
154 end
155
155
156 def totalable?
157 format.totalable_supported
158 end
159
156 # Returns a ORDER BY clause that can used to sort customized
160 # Returns a ORDER BY clause that can used to sort customized
157 # objects by their value of the custom field.
161 # objects by their value of the custom field.
158 # Returns nil if the custom field can not be used for sorting.
162 # Returns nil if the custom field can not be used for sorting.
159 def order_statement
163 def order_statement
160 return nil if multiple?
164 return nil if multiple?
161 format.order_statement(self)
165 format.order_statement(self)
162 end
166 end
163
167
164 # Returns a GROUP BY clause that can used to group by custom value
168 # Returns a GROUP BY clause that can used to group by custom value
165 # Returns nil if the custom field can not be used for grouping.
169 # Returns nil if the custom field can not be used for grouping.
166 def group_statement
170 def group_statement
167 return nil if multiple?
171 return nil if multiple?
168 format.group_statement(self)
172 format.group_statement(self)
169 end
173 end
170
174
171 def join_for_order_statement
175 def join_for_order_statement
172 format.join_for_order_statement(self)
176 format.join_for_order_statement(self)
173 end
177 end
174
178
175 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
179 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
176 if visible? || user.admin?
180 if visible? || user.admin?
177 "1=1"
181 "1=1"
178 elsif user.anonymous?
182 elsif user.anonymous?
179 "1=0"
183 "1=0"
180 else
184 else
181 project_key ||= "#{self.class.customized_class.table_name}.project_id"
185 project_key ||= "#{self.class.customized_class.table_name}.project_id"
182 id_column ||= id
186 id_column ||= id
183 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
187 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
184 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
188 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
185 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
189 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
186 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
190 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
187 end
191 end
188 end
192 end
189
193
190 def self.visibility_condition
194 def self.visibility_condition
191 if user.admin?
195 if user.admin?
192 "1=1"
196 "1=1"
193 elsif user.anonymous?
197 elsif user.anonymous?
194 "#{table_name}.visible"
198 "#{table_name}.visible"
195 else
199 else
196 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
200 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
197 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
201 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
198 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
202 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
199 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
203 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
200 end
204 end
201 end
205 end
202
206
203 def <=>(field)
207 def <=>(field)
204 position <=> field.position
208 position <=> field.position
205 end
209 end
206
210
207 # Returns the class that values represent
211 # Returns the class that values represent
208 def value_class
212 def value_class
209 format.target_class if format.respond_to?(:target_class)
213 format.target_class if format.respond_to?(:target_class)
210 end
214 end
211
215
212 def self.customized_class
216 def self.customized_class
213 self.name =~ /^(.+)CustomField$/
217 self.name =~ /^(.+)CustomField$/
214 $1.constantize rescue nil
218 $1.constantize rescue nil
215 end
219 end
216
220
217 # to move in project_custom_field
221 # to move in project_custom_field
218 def self.for_all
222 def self.for_all
219 where(:is_for_all => true).order('position').to_a
223 where(:is_for_all => true).order('position').to_a
220 end
224 end
221
225
222 def type_name
226 def type_name
223 nil
227 nil
224 end
228 end
225
229
226 # Returns the error messages for the given value
230 # Returns the error messages for the given value
227 # or an empty array if value is a valid value for the custom field
231 # or an empty array if value is a valid value for the custom field
228 def validate_custom_value(custom_value)
232 def validate_custom_value(custom_value)
229 value = custom_value.value
233 value = custom_value.value
230 errs = []
234 errs = []
231 if value.is_a?(Array)
235 if value.is_a?(Array)
232 if !multiple?
236 if !multiple?
233 errs << ::I18n.t('activerecord.errors.messages.invalid')
237 errs << ::I18n.t('activerecord.errors.messages.invalid')
234 end
238 end
235 if is_required? && value.detect(&:present?).nil?
239 if is_required? && value.detect(&:present?).nil?
236 errs << ::I18n.t('activerecord.errors.messages.blank')
240 errs << ::I18n.t('activerecord.errors.messages.blank')
237 end
241 end
238 else
242 else
239 if is_required? && value.blank?
243 if is_required? && value.blank?
240 errs << ::I18n.t('activerecord.errors.messages.blank')
244 errs << ::I18n.t('activerecord.errors.messages.blank')
241 end
245 end
242 end
246 end
243 errs += format.validate_custom_value(custom_value)
247 errs += format.validate_custom_value(custom_value)
244 errs
248 errs
245 end
249 end
246
250
247 # Returns the error messages for the default custom field value
251 # Returns the error messages for the default custom field value
248 def validate_field_value(value)
252 def validate_field_value(value)
249 validate_custom_value(CustomFieldValue.new(:custom_field => self, :value => value))
253 validate_custom_value(CustomFieldValue.new(:custom_field => self, :value => value))
250 end
254 end
251
255
252 # Returns true if value is a valid value for the custom field
256 # Returns true if value is a valid value for the custom field
253 def valid_field_value?(value)
257 def valid_field_value?(value)
254 validate_field_value(value).empty?
258 validate_field_value(value).empty?
255 end
259 end
256
260
257 def format_in?(*args)
261 def format_in?(*args)
258 args.include?(field_format)
262 args.include?(field_format)
259 end
263 end
260
264
261 protected
265 protected
262
266
263 # Removes multiple values for the custom field after setting the multiple attribute to false
267 # Removes multiple values for the custom field after setting the multiple attribute to false
264 # We kepp the value with the highest id for each customized object
268 # We kepp the value with the highest id for each customized object
265 def handle_multiplicity_change
269 def handle_multiplicity_change
266 if !new_record? && multiple_was && !multiple
270 if !new_record? && multiple_was && !multiple
267 ids = custom_values.
271 ids = custom_values.
268 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
272 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
269 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
273 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
270 " AND cve.id > #{CustomValue.table_name}.id)").
274 " AND cve.id > #{CustomValue.table_name}.id)").
271 pluck(:id)
275 pluck(:id)
272
276
273 if ids.any?
277 if ids.any?
274 custom_values.where(:id => ids).delete_all
278 custom_values.where(:id => ids).delete_all
275 end
279 end
276 end
280 end
277 end
281 end
278 end
282 end
279
283
280 require_dependency 'redmine/field_format'
284 require_dependency 'redmine/field_format'
@@ -1,1038 +1,1026
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :totalable, :default_order
19 attr_accessor :name, :sortable, :groupable, :totalable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.totalable = options[:totalable] || false
29 self.totalable = options[:totalable] || false
30 self.default_order = options[:default_order]
30 self.default_order = options[:default_order]
31 @inline = options.key?(:inline) ? options[:inline] : true
31 @inline = options.key?(:inline) ? options[:inline] : true
32 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @caption_key = options[:caption] || "field_#{name}".to_sym
33 @frozen = options[:frozen]
33 @frozen = options[:frozen]
34 end
34 end
35
35
36 def caption
36 def caption
37 case @caption_key
37 case @caption_key
38 when Symbol
38 when Symbol
39 l(@caption_key)
39 l(@caption_key)
40 when Proc
40 when Proc
41 @caption_key.call
41 @caption_key.call
42 else
42 else
43 @caption_key
43 @caption_key
44 end
44 end
45 end
45 end
46
46
47 # Returns true if the column is sortable, otherwise false
47 # Returns true if the column is sortable, otherwise false
48 def sortable?
48 def sortable?
49 !@sortable.nil?
49 !@sortable.nil?
50 end
50 end
51
51
52 def sortable
52 def sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
54 end
54 end
55
55
56 def inline?
56 def inline?
57 @inline
57 @inline
58 end
58 end
59
59
60 def frozen?
60 def frozen?
61 @frozen
61 @frozen
62 end
62 end
63
63
64 def value(object)
64 def value(object)
65 object.send name
65 object.send name
66 end
66 end
67
67
68 def value_object(object)
68 def value_object(object)
69 object.send name
69 object.send name
70 end
70 end
71
71
72 def css_classes
72 def css_classes
73 name
73 name
74 end
74 end
75 end
75 end
76
76
77 class QueryCustomFieldColumn < QueryColumn
77 class QueryCustomFieldColumn < QueryColumn
78
78
79 def initialize(custom_field)
79 def initialize(custom_field)
80 self.name = "cf_#{custom_field.id}".to_sym
80 self.name = "cf_#{custom_field.id}".to_sym
81 self.sortable = custom_field.order_statement || false
81 self.sortable = custom_field.order_statement || false
82 self.groupable = custom_field.group_statement || false
82 self.groupable = custom_field.group_statement || false
83 self.totalable = ['int', 'float'].include?(custom_field.field_format)
83 self.totalable = custom_field.totalable?
84 @inline = true
84 @inline = true
85 @cf = custom_field
85 @cf = custom_field
86 end
86 end
87
87
88 def caption
88 def caption
89 @cf.name
89 @cf.name
90 end
90 end
91
91
92 def custom_field
92 def custom_field
93 @cf
93 @cf
94 end
94 end
95
95
96 def value_object(object)
96 def value_object(object)
97 if custom_field.visible_by?(object.project, User.current)
97 if custom_field.visible_by?(object.project, User.current)
98 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
98 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
99 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
99 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
100 else
100 else
101 nil
101 nil
102 end
102 end
103 end
103 end
104
104
105 def value(object)
105 def value(object)
106 raw = value_object(object)
106 raw = value_object(object)
107 if raw.is_a?(Array)
107 if raw.is_a?(Array)
108 raw.map {|r| @cf.cast_value(r.value)}
108 raw.map {|r| @cf.cast_value(r.value)}
109 elsif raw
109 elsif raw
110 @cf.cast_value(raw.value)
110 @cf.cast_value(raw.value)
111 else
111 else
112 nil
112 nil
113 end
113 end
114 end
114 end
115
115
116 def css_classes
116 def css_classes
117 @css_classes ||= "#{name} #{@cf.field_format}"
117 @css_classes ||= "#{name} #{@cf.field_format}"
118 end
118 end
119 end
119 end
120
120
121 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
121 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
122
122
123 def initialize(association, custom_field)
123 def initialize(association, custom_field)
124 super(custom_field)
124 super(custom_field)
125 self.name = "#{association}.cf_#{custom_field.id}".to_sym
125 self.name = "#{association}.cf_#{custom_field.id}".to_sym
126 # TODO: support sorting/grouping by association custom field
126 # TODO: support sorting/grouping by association custom field
127 self.sortable = false
127 self.sortable = false
128 self.groupable = false
128 self.groupable = false
129 @association = association
129 @association = association
130 end
130 end
131
131
132 def value_object(object)
132 def value_object(object)
133 if assoc = object.send(@association)
133 if assoc = object.send(@association)
134 super(assoc)
134 super(assoc)
135 end
135 end
136 end
136 end
137
137
138 def css_classes
138 def css_classes
139 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
139 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
140 end
140 end
141 end
141 end
142
142
143 class Query < ActiveRecord::Base
143 class Query < ActiveRecord::Base
144 class StatementInvalid < ::ActiveRecord::StatementInvalid
144 class StatementInvalid < ::ActiveRecord::StatementInvalid
145 end
145 end
146
146
147 VISIBILITY_PRIVATE = 0
147 VISIBILITY_PRIVATE = 0
148 VISIBILITY_ROLES = 1
148 VISIBILITY_ROLES = 1
149 VISIBILITY_PUBLIC = 2
149 VISIBILITY_PUBLIC = 2
150
150
151 belongs_to :project
151 belongs_to :project
152 belongs_to :user
152 belongs_to :user
153 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
153 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
154 serialize :filters
154 serialize :filters
155 serialize :column_names
155 serialize :column_names
156 serialize :sort_criteria, Array
156 serialize :sort_criteria, Array
157 serialize :options, Hash
157 serialize :options, Hash
158
158
159 attr_protected :project_id, :user_id
159 attr_protected :project_id, :user_id
160
160
161 validates_presence_of :name
161 validates_presence_of :name
162 validates_length_of :name, :maximum => 255
162 validates_length_of :name, :maximum => 255
163 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
163 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
164 validate :validate_query_filters
164 validate :validate_query_filters
165 validate do |query|
165 validate do |query|
166 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
166 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
167 end
167 end
168
168
169 after_save do |query|
169 after_save do |query|
170 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
170 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
171 query.roles.clear
171 query.roles.clear
172 end
172 end
173 end
173 end
174
174
175 class_attribute :operators
175 class_attribute :operators
176 self.operators = {
176 self.operators = {
177 "=" => :label_equals,
177 "=" => :label_equals,
178 "!" => :label_not_equals,
178 "!" => :label_not_equals,
179 "o" => :label_open_issues,
179 "o" => :label_open_issues,
180 "c" => :label_closed_issues,
180 "c" => :label_closed_issues,
181 "!*" => :label_none,
181 "!*" => :label_none,
182 "*" => :label_any,
182 "*" => :label_any,
183 ">=" => :label_greater_or_equal,
183 ">=" => :label_greater_or_equal,
184 "<=" => :label_less_or_equal,
184 "<=" => :label_less_or_equal,
185 "><" => :label_between,
185 "><" => :label_between,
186 "<t+" => :label_in_less_than,
186 "<t+" => :label_in_less_than,
187 ">t+" => :label_in_more_than,
187 ">t+" => :label_in_more_than,
188 "><t+"=> :label_in_the_next_days,
188 "><t+"=> :label_in_the_next_days,
189 "t+" => :label_in,
189 "t+" => :label_in,
190 "t" => :label_today,
190 "t" => :label_today,
191 "ld" => :label_yesterday,
191 "ld" => :label_yesterday,
192 "w" => :label_this_week,
192 "w" => :label_this_week,
193 "lw" => :label_last_week,
193 "lw" => :label_last_week,
194 "l2w" => [:label_last_n_weeks, {:count => 2}],
194 "l2w" => [:label_last_n_weeks, {:count => 2}],
195 "m" => :label_this_month,
195 "m" => :label_this_month,
196 "lm" => :label_last_month,
196 "lm" => :label_last_month,
197 "y" => :label_this_year,
197 "y" => :label_this_year,
198 ">t-" => :label_less_than_ago,
198 ">t-" => :label_less_than_ago,
199 "<t-" => :label_more_than_ago,
199 "<t-" => :label_more_than_ago,
200 "><t-"=> :label_in_the_past_days,
200 "><t-"=> :label_in_the_past_days,
201 "t-" => :label_ago,
201 "t-" => :label_ago,
202 "~" => :label_contains,
202 "~" => :label_contains,
203 "!~" => :label_not_contains,
203 "!~" => :label_not_contains,
204 "=p" => :label_any_issues_in_project,
204 "=p" => :label_any_issues_in_project,
205 "=!p" => :label_any_issues_not_in_project,
205 "=!p" => :label_any_issues_not_in_project,
206 "!p" => :label_no_issues_in_project,
206 "!p" => :label_no_issues_in_project,
207 "*o" => :label_any_open_issues,
207 "*o" => :label_any_open_issues,
208 "!o" => :label_no_open_issues
208 "!o" => :label_no_open_issues
209 }
209 }
210
210
211 class_attribute :operators_by_filter_type
211 class_attribute :operators_by_filter_type
212 self.operators_by_filter_type = {
212 self.operators_by_filter_type = {
213 :list => [ "=", "!" ],
213 :list => [ "=", "!" ],
214 :list_status => [ "o", "=", "!", "c", "*" ],
214 :list_status => [ "o", "=", "!", "c", "*" ],
215 :list_optional => [ "=", "!", "!*", "*" ],
215 :list_optional => [ "=", "!", "!*", "*" ],
216 :list_subprojects => [ "*", "!*", "=" ],
216 :list_subprojects => [ "*", "!*", "=" ],
217 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
217 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
218 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
218 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
219 :string => [ "=", "~", "!", "!~", "!*", "*" ],
219 :string => [ "=", "~", "!", "!~", "!*", "*" ],
220 :text => [ "~", "!~", "!*", "*" ],
220 :text => [ "~", "!~", "!*", "*" ],
221 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
221 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
222 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
222 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
223 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
223 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
224 :tree => ["=", "~", "!*", "*"]
224 :tree => ["=", "~", "!*", "*"]
225 }
225 }
226
226
227 class_attribute :available_columns
227 class_attribute :available_columns
228 self.available_columns = []
228 self.available_columns = []
229
229
230 class_attribute :queried_class
230 class_attribute :queried_class
231
231
232 def queried_table_name
232 def queried_table_name
233 @queried_table_name ||= self.class.queried_class.table_name
233 @queried_table_name ||= self.class.queried_class.table_name
234 end
234 end
235
235
236 def initialize(attributes=nil, *args)
236 def initialize(attributes=nil, *args)
237 super attributes
237 super attributes
238 @is_for_all = project.nil?
238 @is_for_all = project.nil?
239 end
239 end
240
240
241 # Builds the query from the given params
241 # Builds the query from the given params
242 def build_from_params(params)
242 def build_from_params(params)
243 if params[:fields] || params[:f]
243 if params[:fields] || params[:f]
244 self.filters = {}
244 self.filters = {}
245 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
245 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
246 else
246 else
247 available_filters.keys.each do |field|
247 available_filters.keys.each do |field|
248 add_short_filter(field, params[field]) if params[field]
248 add_short_filter(field, params[field]) if params[field]
249 end
249 end
250 end
250 end
251 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
251 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
252 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
252 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
253 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
253 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
254 self
254 self
255 end
255 end
256
256
257 # Builds a new query from the given params and attributes
257 # Builds a new query from the given params and attributes
258 def self.build_from_params(params, attributes={})
258 def self.build_from_params(params, attributes={})
259 new(attributes).build_from_params(params)
259 new(attributes).build_from_params(params)
260 end
260 end
261
261
262 def validate_query_filters
262 def validate_query_filters
263 filters.each_key do |field|
263 filters.each_key do |field|
264 if values_for(field)
264 if values_for(field)
265 case type_for(field)
265 case type_for(field)
266 when :integer
266 when :integer
267 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
267 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
268 when :float
268 when :float
269 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
269 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
270 when :date, :date_past
270 when :date, :date_past
271 case operator_for(field)
271 case operator_for(field)
272 when "=", ">=", "<=", "><"
272 when "=", ">=", "<=", "><"
273 add_filter_error(field, :invalid) if values_for(field).detect {|v|
273 add_filter_error(field, :invalid) if values_for(field).detect {|v|
274 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?)
274 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?)
275 }
275 }
276 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
276 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
277 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
277 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
278 end
278 end
279 end
279 end
280 end
280 end
281
281
282 add_filter_error(field, :blank) unless
282 add_filter_error(field, :blank) unless
283 # filter requires one or more values
283 # filter requires one or more values
284 (values_for(field) and !values_for(field).first.blank?) or
284 (values_for(field) and !values_for(field).first.blank?) or
285 # filter doesn't require any value
285 # filter doesn't require any value
286 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
286 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
287 end if filters
287 end if filters
288 end
288 end
289
289
290 def add_filter_error(field, message)
290 def add_filter_error(field, message)
291 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
291 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
292 errors.add(:base, m)
292 errors.add(:base, m)
293 end
293 end
294
294
295 def editable_by?(user)
295 def editable_by?(user)
296 return false unless user
296 return false unless user
297 # Admin can edit them all and regular users can edit their private queries
297 # Admin can edit them all and regular users can edit their private queries
298 return true if user.admin? || (is_private? && self.user_id == user.id)
298 return true if user.admin? || (is_private? && self.user_id == user.id)
299 # Members can not edit public queries that are for all project (only admin is allowed to)
299 # Members can not edit public queries that are for all project (only admin is allowed to)
300 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
300 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
301 end
301 end
302
302
303 def trackers
303 def trackers
304 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
304 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
305 end
305 end
306
306
307 # Returns a hash of localized labels for all filter operators
307 # Returns a hash of localized labels for all filter operators
308 def self.operators_labels
308 def self.operators_labels
309 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
309 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
310 end
310 end
311
311
312 # Returns a representation of the available filters for JSON serialization
312 # Returns a representation of the available filters for JSON serialization
313 def available_filters_as_json
313 def available_filters_as_json
314 json = {}
314 json = {}
315 available_filters.each do |field, options|
315 available_filters.each do |field, options|
316 options = options.slice(:type, :name, :values)
316 options = options.slice(:type, :name, :values)
317 if options[:values] && values_for(field)
317 if options[:values] && values_for(field)
318 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
318 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
319 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
319 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
320 options[:values] += send(method, missing)
320 options[:values] += send(method, missing)
321 end
321 end
322 end
322 end
323 json[field] = options.stringify_keys
323 json[field] = options.stringify_keys
324 end
324 end
325 json
325 json
326 end
326 end
327
327
328 def all_projects
328 def all_projects
329 @all_projects ||= Project.visible.to_a
329 @all_projects ||= Project.visible.to_a
330 end
330 end
331
331
332 def all_projects_values
332 def all_projects_values
333 return @all_projects_values if @all_projects_values
333 return @all_projects_values if @all_projects_values
334
334
335 values = []
335 values = []
336 Project.project_tree(all_projects) do |p, level|
336 Project.project_tree(all_projects) do |p, level|
337 prefix = (level > 0 ? ('--' * level + ' ') : '')
337 prefix = (level > 0 ? ('--' * level + ' ') : '')
338 values << ["#{prefix}#{p.name}", p.id.to_s]
338 values << ["#{prefix}#{p.name}", p.id.to_s]
339 end
339 end
340 @all_projects_values = values
340 @all_projects_values = values
341 end
341 end
342
342
343 # Adds available filters
343 # Adds available filters
344 def initialize_available_filters
344 def initialize_available_filters
345 # implemented by sub-classes
345 # implemented by sub-classes
346 end
346 end
347 protected :initialize_available_filters
347 protected :initialize_available_filters
348
348
349 # Adds an available filter
349 # Adds an available filter
350 def add_available_filter(field, options)
350 def add_available_filter(field, options)
351 @available_filters ||= ActiveSupport::OrderedHash.new
351 @available_filters ||= ActiveSupport::OrderedHash.new
352 @available_filters[field] = options
352 @available_filters[field] = options
353 @available_filters
353 @available_filters
354 end
354 end
355
355
356 # Removes an available filter
356 # Removes an available filter
357 def delete_available_filter(field)
357 def delete_available_filter(field)
358 if @available_filters
358 if @available_filters
359 @available_filters.delete(field)
359 @available_filters.delete(field)
360 end
360 end
361 end
361 end
362
362
363 # Return a hash of available filters
363 # Return a hash of available filters
364 def available_filters
364 def available_filters
365 unless @available_filters
365 unless @available_filters
366 initialize_available_filters
366 initialize_available_filters
367 @available_filters.each do |field, options|
367 @available_filters.each do |field, options|
368 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
368 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
369 end
369 end
370 end
370 end
371 @available_filters
371 @available_filters
372 end
372 end
373
373
374 def add_filter(field, operator, values=nil)
374 def add_filter(field, operator, values=nil)
375 # values must be an array
375 # values must be an array
376 return unless values.nil? || values.is_a?(Array)
376 return unless values.nil? || values.is_a?(Array)
377 # check if field is defined as an available filter
377 # check if field is defined as an available filter
378 if available_filters.has_key? field
378 if available_filters.has_key? field
379 filter_options = available_filters[field]
379 filter_options = available_filters[field]
380 filters[field] = {:operator => operator, :values => (values || [''])}
380 filters[field] = {:operator => operator, :values => (values || [''])}
381 end
381 end
382 end
382 end
383
383
384 def add_short_filter(field, expression)
384 def add_short_filter(field, expression)
385 return unless expression && available_filters.has_key?(field)
385 return unless expression && available_filters.has_key?(field)
386 field_type = available_filters[field][:type]
386 field_type = available_filters[field][:type]
387 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
387 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
388 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
388 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
389 values = $1
389 values = $1
390 add_filter field, operator, values.present? ? values.split('|') : ['']
390 add_filter field, operator, values.present? ? values.split('|') : ['']
391 end || add_filter(field, '=', expression.split('|'))
391 end || add_filter(field, '=', expression.split('|'))
392 end
392 end
393
393
394 # Add multiple filters using +add_filter+
394 # Add multiple filters using +add_filter+
395 def add_filters(fields, operators, values)
395 def add_filters(fields, operators, values)
396 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
396 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
397 fields.each do |field|
397 fields.each do |field|
398 add_filter(field, operators[field], values && values[field])
398 add_filter(field, operators[field], values && values[field])
399 end
399 end
400 end
400 end
401 end
401 end
402
402
403 def has_filter?(field)
403 def has_filter?(field)
404 filters and filters[field]
404 filters and filters[field]
405 end
405 end
406
406
407 def type_for(field)
407 def type_for(field)
408 available_filters[field][:type] if available_filters.has_key?(field)
408 available_filters[field][:type] if available_filters.has_key?(field)
409 end
409 end
410
410
411 def operator_for(field)
411 def operator_for(field)
412 has_filter?(field) ? filters[field][:operator] : nil
412 has_filter?(field) ? filters[field][:operator] : nil
413 end
413 end
414
414
415 def values_for(field)
415 def values_for(field)
416 has_filter?(field) ? filters[field][:values] : nil
416 has_filter?(field) ? filters[field][:values] : nil
417 end
417 end
418
418
419 def value_for(field, index=0)
419 def value_for(field, index=0)
420 (values_for(field) || [])[index]
420 (values_for(field) || [])[index]
421 end
421 end
422
422
423 def label_for(field)
423 def label_for(field)
424 label = available_filters[field][:name] if available_filters.has_key?(field)
424 label = available_filters[field][:name] if available_filters.has_key?(field)
425 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
425 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
426 end
426 end
427
427
428 def self.add_available_column(column)
428 def self.add_available_column(column)
429 self.available_columns << (column) if column.is_a?(QueryColumn)
429 self.available_columns << (column) if column.is_a?(QueryColumn)
430 end
430 end
431
431
432 # Returns an array of columns that can be used to group the results
432 # Returns an array of columns that can be used to group the results
433 def groupable_columns
433 def groupable_columns
434 available_columns.select {|c| c.groupable}
434 available_columns.select {|c| c.groupable}
435 end
435 end
436
436
437 # Returns a Hash of columns and the key for sorting
437 # Returns a Hash of columns and the key for sorting
438 def sortable_columns
438 def sortable_columns
439 available_columns.inject({}) {|h, column|
439 available_columns.inject({}) {|h, column|
440 h[column.name.to_s] = column.sortable
440 h[column.name.to_s] = column.sortable
441 h
441 h
442 }
442 }
443 end
443 end
444
444
445 def columns
445 def columns
446 # preserve the column_names order
446 # preserve the column_names order
447 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
447 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
448 available_columns.find { |col| col.name == name }
448 available_columns.find { |col| col.name == name }
449 end.compact
449 end.compact
450 available_columns.select(&:frozen?) | cols
450 available_columns.select(&:frozen?) | cols
451 end
451 end
452
452
453 def inline_columns
453 def inline_columns
454 columns.select(&:inline?)
454 columns.select(&:inline?)
455 end
455 end
456
456
457 def block_columns
457 def block_columns
458 columns.reject(&:inline?)
458 columns.reject(&:inline?)
459 end
459 end
460
460
461 def available_inline_columns
461 def available_inline_columns
462 available_columns.select(&:inline?)
462 available_columns.select(&:inline?)
463 end
463 end
464
464
465 def available_block_columns
465 def available_block_columns
466 available_columns.reject(&:inline?)
466 available_columns.reject(&:inline?)
467 end
467 end
468
468
469 def available_totalable_columns
469 def available_totalable_columns
470 available_columns.select(&:totalable)
470 available_columns.select(&:totalable)
471 end
471 end
472
472
473 def default_columns_names
473 def default_columns_names
474 []
474 []
475 end
475 end
476
476
477 def column_names=(names)
477 def column_names=(names)
478 if names
478 if names
479 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
479 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
480 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
480 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
481 # Set column_names to nil if default columns
481 # Set column_names to nil if default columns
482 if names == default_columns_names
482 if names == default_columns_names
483 names = nil
483 names = nil
484 end
484 end
485 end
485 end
486 write_attribute(:column_names, names)
486 write_attribute(:column_names, names)
487 end
487 end
488
488
489 def has_column?(column)
489 def has_column?(column)
490 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
490 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
491 end
491 end
492
492
493 def has_custom_field_column?
493 def has_custom_field_column?
494 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
494 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
495 end
495 end
496
496
497 def has_default_columns?
497 def has_default_columns?
498 column_names.nil? || column_names.empty?
498 column_names.nil? || column_names.empty?
499 end
499 end
500
500
501 def totalable_columns
501 def totalable_columns
502 names = totalable_names
502 names = totalable_names
503 available_totalable_columns.select {|column| names.include?(column.name)}
503 available_totalable_columns.select {|column| names.include?(column.name)}
504 end
504 end
505
505
506 def totalable_names=(names)
506 def totalable_names=(names)
507 if names
507 if names
508 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
508 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
509 end
509 end
510 options[:totalable_names] = names
510 options[:totalable_names] = names
511 end
511 end
512
512
513 def totalable_names
513 def totalable_names
514 options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || []
514 options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || []
515 end
515 end
516
516
517 def sort_criteria=(arg)
517 def sort_criteria=(arg)
518 c = []
518 c = []
519 if arg.is_a?(Hash)
519 if arg.is_a?(Hash)
520 arg = arg.keys.sort.collect {|k| arg[k]}
520 arg = arg.keys.sort.collect {|k| arg[k]}
521 end
521 end
522 if arg
522 if arg
523 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
523 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
524 end
524 end
525 write_attribute(:sort_criteria, c)
525 write_attribute(:sort_criteria, c)
526 end
526 end
527
527
528 def sort_criteria
528 def sort_criteria
529 read_attribute(:sort_criteria) || []
529 read_attribute(:sort_criteria) || []
530 end
530 end
531
531
532 def sort_criteria_key(arg)
532 def sort_criteria_key(arg)
533 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
533 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
534 end
534 end
535
535
536 def sort_criteria_order(arg)
536 def sort_criteria_order(arg)
537 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
537 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
538 end
538 end
539
539
540 def sort_criteria_order_for(key)
540 def sort_criteria_order_for(key)
541 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
541 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
542 end
542 end
543
543
544 # Returns the SQL sort order that should be prepended for grouping
544 # Returns the SQL sort order that should be prepended for grouping
545 def group_by_sort_order
545 def group_by_sort_order
546 if grouped? && (column = group_by_column)
546 if grouped? && (column = group_by_column)
547 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
547 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
548 column.sortable.is_a?(Array) ?
548 column.sortable.is_a?(Array) ?
549 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
549 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
550 "#{column.sortable} #{order}"
550 "#{column.sortable} #{order}"
551 end
551 end
552 end
552 end
553
553
554 # Returns true if the query is a grouped query
554 # Returns true if the query is a grouped query
555 def grouped?
555 def grouped?
556 !group_by_column.nil?
556 !group_by_column.nil?
557 end
557 end
558
558
559 def group_by_column
559 def group_by_column
560 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
560 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
561 end
561 end
562
562
563 def group_by_statement
563 def group_by_statement
564 group_by_column.try(:groupable)
564 group_by_column.try(:groupable)
565 end
565 end
566
566
567 def project_statement
567 def project_statement
568 project_clauses = []
568 project_clauses = []
569 if project && !project.descendants.active.empty?
569 if project && !project.descendants.active.empty?
570 ids = [project.id]
570 ids = [project.id]
571 if has_filter?("subproject_id")
571 if has_filter?("subproject_id")
572 case operator_for("subproject_id")
572 case operator_for("subproject_id")
573 when '='
573 when '='
574 # include the selected subprojects
574 # include the selected subprojects
575 ids += values_for("subproject_id").each(&:to_i)
575 ids += values_for("subproject_id").each(&:to_i)
576 when '!*'
576 when '!*'
577 # main project only
577 # main project only
578 else
578 else
579 # all subprojects
579 # all subprojects
580 ids += project.descendants.collect(&:id)
580 ids += project.descendants.collect(&:id)
581 end
581 end
582 elsif Setting.display_subprojects_issues?
582 elsif Setting.display_subprojects_issues?
583 ids += project.descendants.collect(&:id)
583 ids += project.descendants.collect(&:id)
584 end
584 end
585 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
585 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
586 elsif project
586 elsif project
587 project_clauses << "#{Project.table_name}.id = %d" % project.id
587 project_clauses << "#{Project.table_name}.id = %d" % project.id
588 end
588 end
589 project_clauses.any? ? project_clauses.join(' AND ') : nil
589 project_clauses.any? ? project_clauses.join(' AND ') : nil
590 end
590 end
591
591
592 def statement
592 def statement
593 # filters clauses
593 # filters clauses
594 filters_clauses = []
594 filters_clauses = []
595 filters.each_key do |field|
595 filters.each_key do |field|
596 next if field == "subproject_id"
596 next if field == "subproject_id"
597 v = values_for(field).clone
597 v = values_for(field).clone
598 next unless v and !v.empty?
598 next unless v and !v.empty?
599 operator = operator_for(field)
599 operator = operator_for(field)
600
600
601 # "me" value substitution
601 # "me" value substitution
602 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
602 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
603 if v.delete("me")
603 if v.delete("me")
604 if User.current.logged?
604 if User.current.logged?
605 v.push(User.current.id.to_s)
605 v.push(User.current.id.to_s)
606 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
606 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
607 else
607 else
608 v.push("0")
608 v.push("0")
609 end
609 end
610 end
610 end
611 end
611 end
612
612
613 if field == 'project_id'
613 if field == 'project_id'
614 if v.delete('mine')
614 if v.delete('mine')
615 v += User.current.memberships.map(&:project_id).map(&:to_s)
615 v += User.current.memberships.map(&:project_id).map(&:to_s)
616 end
616 end
617 end
617 end
618
618
619 if field =~ /cf_(\d+)$/
619 if field =~ /cf_(\d+)$/
620 # custom field
620 # custom field
621 filters_clauses << sql_for_custom_field(field, operator, v, $1)
621 filters_clauses << sql_for_custom_field(field, operator, v, $1)
622 elsif respond_to?("sql_for_#{field}_field")
622 elsif respond_to?("sql_for_#{field}_field")
623 # specific statement
623 # specific statement
624 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
624 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
625 else
625 else
626 # regular field
626 # regular field
627 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
627 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
628 end
628 end
629 end if filters and valid?
629 end if filters and valid?
630
630
631 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
631 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
632 # Excludes results for which the grouped custom field is not visible
632 # Excludes results for which the grouped custom field is not visible
633 filters_clauses << c.custom_field.visibility_by_project_condition
633 filters_clauses << c.custom_field.visibility_by_project_condition
634 end
634 end
635
635
636 filters_clauses << project_statement
636 filters_clauses << project_statement
637 filters_clauses.reject!(&:blank?)
637 filters_clauses.reject!(&:blank?)
638
638
639 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
639 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
640 end
640 end
641
641
642 # Returns the sum of values for the given column
642 # Returns the sum of values for the given column
643 def total_for(column)
643 def total_for(column)
644 total_with_scope(column, base_scope)
644 total_with_scope(column, base_scope)
645 end
645 end
646
646
647 # Returns a hash of the sum of the given column for each group,
647 # Returns a hash of the sum of the given column for each group,
648 # or nil if the query is not grouped
648 # or nil if the query is not grouped
649 def total_by_group_for(column)
649 def total_by_group_for(column)
650 grouped_query do |scope|
650 grouped_query do |scope|
651 total_with_scope(column, scope)
651 total_with_scope(column, scope)
652 end
652 end
653 end
653 end
654
654
655 def totals
655 def totals
656 totals = totalable_columns.map {|column| [column, total_for(column)]}
656 totals = totalable_columns.map {|column| [column, total_for(column)]}
657 yield totals if block_given?
657 yield totals if block_given?
658 totals
658 totals
659 end
659 end
660
660
661 def totals_by_group
661 def totals_by_group
662 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
662 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
663 yield totals if block_given?
663 yield totals if block_given?
664 totals
664 totals
665 end
665 end
666
666
667 private
667 private
668
668
669 def grouped_query(&block)
669 def grouped_query(&block)
670 r = nil
670 r = nil
671 if grouped?
671 if grouped?
672 begin
672 begin
673 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
673 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
674 r = yield base_group_scope
674 r = yield base_group_scope
675 rescue ActiveRecord::RecordNotFound
675 rescue ActiveRecord::RecordNotFound
676 r = {nil => yield(base_scope)}
676 r = {nil => yield(base_scope)}
677 end
677 end
678 c = group_by_column
678 c = group_by_column
679 if c.is_a?(QueryCustomFieldColumn)
679 if c.is_a?(QueryCustomFieldColumn)
680 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
680 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
681 end
681 end
682 end
682 end
683 r
683 r
684 rescue ::ActiveRecord::StatementInvalid => e
684 rescue ::ActiveRecord::StatementInvalid => e
685 raise StatementInvalid.new(e.message)
685 raise StatementInvalid.new(e.message)
686 end
686 end
687
687
688 def total_with_scope(column, scope)
688 def total_with_scope(column, scope)
689 unless column.is_a?(QueryColumn)
689 unless column.is_a?(QueryColumn)
690 column = column.to_sym
690 column = column.to_sym
691 column = available_totalable_columns.detect {|c| c.name == column}
691 column = available_totalable_columns.detect {|c| c.name == column}
692 end
692 end
693 if column.is_a?(QueryCustomFieldColumn)
693 if column.is_a?(QueryCustomFieldColumn)
694 custom_field = column.custom_field
694 custom_field = column.custom_field
695 send "total_for_#{custom_field.field_format}_custom_field", custom_field, scope
695 send "total_for_custom_field", custom_field, scope
696 else
696 else
697 send "total_for_#{column.name}", scope
697 send "total_for_#{column.name}", scope
698 end
698 end
699 rescue ::ActiveRecord::StatementInvalid => e
699 rescue ::ActiveRecord::StatementInvalid => e
700 raise StatementInvalid.new(e.message)
700 raise StatementInvalid.new(e.message)
701 end
701 end
702
702
703 def base_scope
703 def base_scope
704 raise "unimplemented"
704 raise "unimplemented"
705 end
705 end
706
706
707 def base_group_scope
707 def base_group_scope
708 base_scope.
708 base_scope.
709 joins(joins_for_order_statement(group_by_statement)).
709 joins(joins_for_order_statement(group_by_statement)).
710 group(group_by_statement)
710 group(group_by_statement)
711 end
711 end
712
712
713 def total_for_float_custom_field(custom_field, scope)
714 total_for_custom_field(custom_field, scope) {|t| t.to_f.round(2)}
715 end
716
717 def total_for_int_custom_field(custom_field, scope)
718 total_for_custom_field(custom_field, scope) {|t| t.to_i}
719 end
720
721 def total_for_custom_field(custom_field, scope, &block)
713 def total_for_custom_field(custom_field, scope, &block)
722 total = scope.joins(:custom_values).
714 total = custom_field.format.total_for_scope(custom_field, scope)
723 where(:custom_values => {:custom_field_id => custom_field.id}).
715 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
724 where.not(:custom_values => {:value => ''}).
725 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
726
727 total = map_total(total, &block) if block_given?
728 total
716 total
729 end
717 end
730
718
731 def map_total(total, &block)
719 def map_total(total, &block)
732 if total.is_a?(Hash)
720 if total.is_a?(Hash)
733 total.keys.each {|k| total[k] = yield total[k]}
721 total.keys.each {|k| total[k] = yield total[k]}
734 else
722 else
735 total = yield total
723 total = yield total
736 end
724 end
737 total
725 total
738 end
726 end
739
727
740 def sql_for_custom_field(field, operator, value, custom_field_id)
728 def sql_for_custom_field(field, operator, value, custom_field_id)
741 db_table = CustomValue.table_name
729 db_table = CustomValue.table_name
742 db_field = 'value'
730 db_field = 'value'
743 filter = @available_filters[field]
731 filter = @available_filters[field]
744 return nil unless filter
732 return nil unless filter
745 if filter[:field].format.target_class && filter[:field].format.target_class <= User
733 if filter[:field].format.target_class && filter[:field].format.target_class <= User
746 if value.delete('me')
734 if value.delete('me')
747 value.push User.current.id.to_s
735 value.push User.current.id.to_s
748 end
736 end
749 end
737 end
750 not_in = nil
738 not_in = nil
751 if operator == '!'
739 if operator == '!'
752 # Makes ! operator work for custom fields with multiple values
740 # Makes ! operator work for custom fields with multiple values
753 operator = '='
741 operator = '='
754 not_in = 'NOT'
742 not_in = 'NOT'
755 end
743 end
756 customized_key = "id"
744 customized_key = "id"
757 customized_class = queried_class
745 customized_class = queried_class
758 if field =~ /^(.+)\.cf_/
746 if field =~ /^(.+)\.cf_/
759 assoc = $1
747 assoc = $1
760 customized_key = "#{assoc}_id"
748 customized_key = "#{assoc}_id"
761 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
749 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
762 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
750 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
763 end
751 end
764 where = sql_for_field(field, operator, value, db_table, db_field, true)
752 where = sql_for_field(field, operator, value, db_table, db_field, true)
765 if operator =~ /[<>]/
753 if operator =~ /[<>]/
766 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
754 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
767 end
755 end
768 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
756 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
769 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
757 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
770 " 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}" +
758 " 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}" +
771 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
759 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
772 end
760 end
773
761
774 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
762 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
775 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
763 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
776 sql = ''
764 sql = ''
777 case operator
765 case operator
778 when "="
766 when "="
779 if value.any?
767 if value.any?
780 case type_for(field)
768 case type_for(field)
781 when :date, :date_past
769 when :date, :date_past
782 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
770 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
783 when :integer
771 when :integer
784 if is_custom_filter
772 if is_custom_filter
785 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_i})"
773 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_i})"
786 else
774 else
787 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
775 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
788 end
776 end
789 when :float
777 when :float
790 if is_custom_filter
778 if is_custom_filter
791 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})"
779 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})"
792 else
780 else
793 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
781 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
794 end
782 end
795 else
783 else
796 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
784 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
797 end
785 end
798 else
786 else
799 # IN an empty set
787 # IN an empty set
800 sql = "1=0"
788 sql = "1=0"
801 end
789 end
802 when "!"
790 when "!"
803 if value.any?
791 if value.any?
804 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + "))"
792 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + "))"
805 else
793 else
806 # NOT IN an empty set
794 # NOT IN an empty set
807 sql = "1=1"
795 sql = "1=1"
808 end
796 end
809 when "!*"
797 when "!*"
810 sql = "#{db_table}.#{db_field} IS NULL"
798 sql = "#{db_table}.#{db_field} IS NULL"
811 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
799 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
812 when "*"
800 when "*"
813 sql = "#{db_table}.#{db_field} IS NOT NULL"
801 sql = "#{db_table}.#{db_field} IS NOT NULL"
814 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
802 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
815 when ">="
803 when ">="
816 if [:date, :date_past].include?(type_for(field))
804 if [:date, :date_past].include?(type_for(field))
817 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
805 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
818 else
806 else
819 if is_custom_filter
807 if is_custom_filter
820 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})"
808 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})"
821 else
809 else
822 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
810 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
823 end
811 end
824 end
812 end
825 when "<="
813 when "<="
826 if [:date, :date_past].include?(type_for(field))
814 if [:date, :date_past].include?(type_for(field))
827 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
815 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
828 else
816 else
829 if is_custom_filter
817 if is_custom_filter
830 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})"
818 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})"
831 else
819 else
832 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
820 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
833 end
821 end
834 end
822 end
835 when "><"
823 when "><"
836 if [:date, :date_past].include?(type_for(field))
824 if [:date, :date_past].include?(type_for(field))
837 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
825 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
838 else
826 else
839 if is_custom_filter
827 if is_custom_filter
840 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})"
828 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})"
841 else
829 else
842 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
830 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
843 end
831 end
844 end
832 end
845 when "o"
833 when "o"
846 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"
834 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"
847 when "c"
835 when "c"
848 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"
836 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"
849 when "><t-"
837 when "><t-"
850 # between today - n days and today
838 # between today - n days and today
851 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
839 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
852 when ">t-"
840 when ">t-"
853 # >= today - n days
841 # >= today - n days
854 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
842 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
855 when "<t-"
843 when "<t-"
856 # <= today - n days
844 # <= today - n days
857 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
845 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
858 when "t-"
846 when "t-"
859 # = n days in past
847 # = n days in past
860 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
848 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
861 when "><t+"
849 when "><t+"
862 # between today and today + n days
850 # between today and today + n days
863 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
851 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
864 when ">t+"
852 when ">t+"
865 # >= today + n days
853 # >= today + n days
866 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
854 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
867 when "<t+"
855 when "<t+"
868 # <= today + n days
856 # <= today + n days
869 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
857 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
870 when "t+"
858 when "t+"
871 # = today + n days
859 # = today + n days
872 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
860 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
873 when "t"
861 when "t"
874 # = today
862 # = today
875 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
863 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
876 when "ld"
864 when "ld"
877 # = yesterday
865 # = yesterday
878 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
866 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
879 when "w"
867 when "w"
880 # = this week
868 # = this week
881 first_day_of_week = l(:general_first_day_of_week).to_i
869 first_day_of_week = l(:general_first_day_of_week).to_i
882 day_of_week = Date.today.cwday
870 day_of_week = Date.today.cwday
883 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
871 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
884 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
872 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
885 when "lw"
873 when "lw"
886 # = last week
874 # = last week
887 first_day_of_week = l(:general_first_day_of_week).to_i
875 first_day_of_week = l(:general_first_day_of_week).to_i
888 day_of_week = Date.today.cwday
876 day_of_week = Date.today.cwday
889 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
877 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
890 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
878 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
891 when "l2w"
879 when "l2w"
892 # = last 2 weeks
880 # = last 2 weeks
893 first_day_of_week = l(:general_first_day_of_week).to_i
881 first_day_of_week = l(:general_first_day_of_week).to_i
894 day_of_week = Date.today.cwday
882 day_of_week = Date.today.cwday
895 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
883 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
896 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
884 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
897 when "m"
885 when "m"
898 # = this month
886 # = this month
899 date = Date.today
887 date = Date.today
900 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
888 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
901 when "lm"
889 when "lm"
902 # = last month
890 # = last month
903 date = Date.today.prev_month
891 date = Date.today.prev_month
904 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
892 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
905 when "y"
893 when "y"
906 # = this year
894 # = this year
907 date = Date.today
895 date = Date.today
908 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
896 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
909 when "~"
897 when "~"
910 sql = sql_contains("#{db_table}.#{db_field}", value.first)
898 sql = sql_contains("#{db_table}.#{db_field}", value.first)
911 when "!~"
899 when "!~"
912 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
900 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
913 else
901 else
914 raise "Unknown query operator #{operator}"
902 raise "Unknown query operator #{operator}"
915 end
903 end
916
904
917 return sql
905 return sql
918 end
906 end
919
907
920 # Returns a SQL LIKE statement with wildcards
908 # Returns a SQL LIKE statement with wildcards
921 def sql_contains(db_field, value, match=true)
909 def sql_contains(db_field, value, match=true)
922 value = "'%#{self.class.connection.quote_string(value.to_s)}%'"
910 value = "'%#{self.class.connection.quote_string(value.to_s)}%'"
923 Redmine::Database.like(db_field, value, :match => match)
911 Redmine::Database.like(db_field, value, :match => match)
924 end
912 end
925
913
926 # Adds a filter for the given custom field
914 # Adds a filter for the given custom field
927 def add_custom_field_filter(field, assoc=nil)
915 def add_custom_field_filter(field, assoc=nil)
928 options = field.query_filter_options(self)
916 options = field.query_filter_options(self)
929 if field.format.target_class && field.format.target_class <= User
917 if field.format.target_class && field.format.target_class <= User
930 if options[:values].is_a?(Array) && User.current.logged?
918 if options[:values].is_a?(Array) && User.current.logged?
931 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
919 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
932 end
920 end
933 end
921 end
934
922
935 filter_id = "cf_#{field.id}"
923 filter_id = "cf_#{field.id}"
936 filter_name = field.name
924 filter_name = field.name
937 if assoc.present?
925 if assoc.present?
938 filter_id = "#{assoc}.#{filter_id}"
926 filter_id = "#{assoc}.#{filter_id}"
939 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
927 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
940 end
928 end
941 add_available_filter filter_id, options.merge({
929 add_available_filter filter_id, options.merge({
942 :name => filter_name,
930 :name => filter_name,
943 :field => field
931 :field => field
944 })
932 })
945 end
933 end
946
934
947 # Adds filters for the given custom fields scope
935 # Adds filters for the given custom fields scope
948 def add_custom_fields_filters(scope, assoc=nil)
936 def add_custom_fields_filters(scope, assoc=nil)
949 scope.visible.where(:is_filter => true).sorted.each do |field|
937 scope.visible.where(:is_filter => true).sorted.each do |field|
950 add_custom_field_filter(field, assoc)
938 add_custom_field_filter(field, assoc)
951 end
939 end
952 end
940 end
953
941
954 # Adds filters for the given associations custom fields
942 # Adds filters for the given associations custom fields
955 def add_associations_custom_fields_filters(*associations)
943 def add_associations_custom_fields_filters(*associations)
956 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
944 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
957 associations.each do |assoc|
945 associations.each do |assoc|
958 association_klass = queried_class.reflect_on_association(assoc).klass
946 association_klass = queried_class.reflect_on_association(assoc).klass
959 fields_by_class.each do |field_class, fields|
947 fields_by_class.each do |field_class, fields|
960 if field_class.customized_class <= association_klass
948 if field_class.customized_class <= association_klass
961 fields.sort.each do |field|
949 fields.sort.each do |field|
962 add_custom_field_filter(field, assoc)
950 add_custom_field_filter(field, assoc)
963 end
951 end
964 end
952 end
965 end
953 end
966 end
954 end
967 end
955 end
968
956
969 def quoted_time(time, is_custom_filter)
957 def quoted_time(time, is_custom_filter)
970 if is_custom_filter
958 if is_custom_filter
971 # Custom field values are stored as strings in the DB
959 # Custom field values are stored as strings in the DB
972 # using this format that does not depend on DB date representation
960 # using this format that does not depend on DB date representation
973 time.strftime("%Y-%m-%d %H:%M:%S")
961 time.strftime("%Y-%m-%d %H:%M:%S")
974 else
962 else
975 self.class.connection.quoted_date(time)
963 self.class.connection.quoted_date(time)
976 end
964 end
977 end
965 end
978
966
979 # Returns a SQL clause for a date or datetime field.
967 # Returns a SQL clause for a date or datetime field.
980 def date_clause(table, field, from, to, is_custom_filter)
968 def date_clause(table, field, from, to, is_custom_filter)
981 s = []
969 s = []
982 if from
970 if from
983 if from.is_a?(Date)
971 if from.is_a?(Date)
984 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
972 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
985 else
973 else
986 from = from - 1 # second
974 from = from - 1 # second
987 end
975 end
988 if self.class.default_timezone == :utc
976 if self.class.default_timezone == :utc
989 from = from.utc
977 from = from.utc
990 end
978 end
991 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
979 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
992 end
980 end
993 if to
981 if to
994 if to.is_a?(Date)
982 if to.is_a?(Date)
995 to = Time.local(to.year, to.month, to.day).end_of_day
983 to = Time.local(to.year, to.month, to.day).end_of_day
996 end
984 end
997 if self.class.default_timezone == :utc
985 if self.class.default_timezone == :utc
998 to = to.utc
986 to = to.utc
999 end
987 end
1000 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
988 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1001 end
989 end
1002 s.join(' AND ')
990 s.join(' AND ')
1003 end
991 end
1004
992
1005 # Returns a SQL clause for a date or datetime field using relative dates.
993 # Returns a SQL clause for a date or datetime field using relative dates.
1006 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
994 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1007 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter)
995 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter)
1008 end
996 end
1009
997
1010 # Returns a Date or Time from the given filter value
998 # Returns a Date or Time from the given filter value
1011 def parse_date(arg)
999 def parse_date(arg)
1012 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1000 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1013 Time.parse(arg) rescue nil
1001 Time.parse(arg) rescue nil
1014 else
1002 else
1015 Date.parse(arg) rescue nil
1003 Date.parse(arg) rescue nil
1016 end
1004 end
1017 end
1005 end
1018
1006
1019 # Additional joins required for the given sort options
1007 # Additional joins required for the given sort options
1020 def joins_for_order_statement(order_options)
1008 def joins_for_order_statement(order_options)
1021 joins = []
1009 joins = []
1022
1010
1023 if order_options
1011 if order_options
1024 if order_options.include?('authors')
1012 if order_options.include?('authors')
1025 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1013 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1026 end
1014 end
1027 order_options.scan(/cf_\d+/).uniq.each do |name|
1015 order_options.scan(/cf_\d+/).uniq.each do |name|
1028 column = available_columns.detect {|c| c.name.to_s == name}
1016 column = available_columns.detect {|c| c.name.to_s == name}
1029 join = column && column.custom_field.join_for_order_statement
1017 join = column && column.custom_field.join_for_order_statement
1030 if join
1018 if join
1031 joins << join
1019 joins << join
1032 end
1020 end
1033 end
1021 end
1034 end
1022 end
1035
1023
1036 joins.any? ? joins.join(' ') : nil
1024 joins.any? ? joins.join(' ') : nil
1037 end
1025 end
1038 end
1026 end
@@ -1,784 +1,805
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 module Redmine
18 module Redmine
19 module FieldFormat
19 module FieldFormat
20 def self.add(name, klass)
20 def self.add(name, klass)
21 all[name.to_s] = klass.instance
21 all[name.to_s] = klass.instance
22 end
22 end
23
23
24 def self.delete(name)
24 def self.delete(name)
25 all.delete(name.to_s)
25 all.delete(name.to_s)
26 end
26 end
27
27
28 def self.all
28 def self.all
29 @formats ||= Hash.new(Base.instance)
29 @formats ||= Hash.new(Base.instance)
30 end
30 end
31
31
32 def self.available_formats
32 def self.available_formats
33 all.keys
33 all.keys
34 end
34 end
35
35
36 def self.find(name)
36 def self.find(name)
37 all[name.to_s]
37 all[name.to_s]
38 end
38 end
39
39
40 # Return an array of custom field formats which can be used in select_tag
40 # Return an array of custom field formats which can be used in select_tag
41 def self.as_select(class_name=nil)
41 def self.as_select(class_name=nil)
42 formats = all.values.select do |format|
42 formats = all.values.select do |format|
43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
44 end
44 end
45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
46 end
46 end
47
47
48 class Base
48 class Base
49 include Singleton
49 include Singleton
50 include Redmine::I18n
50 include Redmine::I18n
51 include ERB::Util
51 include ERB::Util
52
52
53 class_attribute :format_name
53 class_attribute :format_name
54 self.format_name = nil
54 self.format_name = nil
55
55
56 # Set this to true if the format supports multiple values
56 # Set this to true if the format supports multiple values
57 class_attribute :multiple_supported
57 class_attribute :multiple_supported
58 self.multiple_supported = false
58 self.multiple_supported = false
59
59
60 # Set this to true if the format supports textual search on custom values
60 # Set this to true if the format supports textual search on custom values
61 class_attribute :searchable_supported
61 class_attribute :searchable_supported
62 self.searchable_supported = false
62 self.searchable_supported = false
63
63
64 # Set this to true if field values can be summed up
65 class_attribute :totalable_supported
66 self.totalable_supported = false
67
64 # Restricts the classes that the custom field can be added to
68 # Restricts the classes that the custom field can be added to
65 # Set to nil for no restrictions
69 # Set to nil for no restrictions
66 class_attribute :customized_class_names
70 class_attribute :customized_class_names
67 self.customized_class_names = nil
71 self.customized_class_names = nil
68
72
69 # Name of the partial for editing the custom field
73 # Name of the partial for editing the custom field
70 class_attribute :form_partial
74 class_attribute :form_partial
71 self.form_partial = nil
75 self.form_partial = nil
72
76
73 class_attribute :change_as_diff
77 class_attribute :change_as_diff
74 self.change_as_diff = false
78 self.change_as_diff = false
75
79
76 def self.add(name)
80 def self.add(name)
77 self.format_name = name
81 self.format_name = name
78 Redmine::FieldFormat.add(name, self)
82 Redmine::FieldFormat.add(name, self)
79 end
83 end
80 private_class_method :add
84 private_class_method :add
81
85
82 def self.field_attributes(*args)
86 def self.field_attributes(*args)
83 CustomField.store_accessor :format_store, *args
87 CustomField.store_accessor :format_store, *args
84 end
88 end
85
89
86 field_attributes :url_pattern
90 field_attributes :url_pattern
87
91
88 def name
92 def name
89 self.class.format_name
93 self.class.format_name
90 end
94 end
91
95
92 def label
96 def label
93 "label_#{name}"
97 "label_#{name}"
94 end
98 end
95
99
96 def cast_custom_value(custom_value)
100 def cast_custom_value(custom_value)
97 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
101 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
98 end
102 end
99
103
100 def cast_value(custom_field, value, customized=nil)
104 def cast_value(custom_field, value, customized=nil)
101 if value.blank?
105 if value.blank?
102 nil
106 nil
103 elsif value.is_a?(Array)
107 elsif value.is_a?(Array)
104 casted = value.map do |v|
108 casted = value.map do |v|
105 cast_single_value(custom_field, v, customized)
109 cast_single_value(custom_field, v, customized)
106 end
110 end
107 casted.compact.sort
111 casted.compact.sort
108 else
112 else
109 cast_single_value(custom_field, value, customized)
113 cast_single_value(custom_field, value, customized)
110 end
114 end
111 end
115 end
112
116
113 def cast_single_value(custom_field, value, customized=nil)
117 def cast_single_value(custom_field, value, customized=nil)
114 value.to_s
118 value.to_s
115 end
119 end
116
120
117 def target_class
121 def target_class
118 nil
122 nil
119 end
123 end
120
124
121 def possible_custom_value_options(custom_value)
125 def possible_custom_value_options(custom_value)
122 possible_values_options(custom_value.custom_field, custom_value.customized)
126 possible_values_options(custom_value.custom_field, custom_value.customized)
123 end
127 end
124
128
125 def possible_values_options(custom_field, object=nil)
129 def possible_values_options(custom_field, object=nil)
126 []
130 []
127 end
131 end
128
132
129 def value_from_keyword(custom_field, keyword, object)
133 def value_from_keyword(custom_field, keyword, object)
130 possible_values_options = possible_values_options(custom_field, object)
134 possible_values_options = possible_values_options(custom_field, object)
131 if possible_values_options.present?
135 if possible_values_options.present?
132 keyword = keyword.to_s
136 keyword = keyword.to_s
133 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
137 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
134 if v.is_a?(Array)
138 if v.is_a?(Array)
135 v.last
139 v.last
136 else
140 else
137 v
141 v
138 end
142 end
139 end
143 end
140 else
144 else
141 keyword
145 keyword
142 end
146 end
143 end
147 end
144
148
145 # Returns the validation errors for custom_field
149 # Returns the validation errors for custom_field
146 # Should return an empty array if custom_field is valid
150 # Should return an empty array if custom_field is valid
147 def validate_custom_field(custom_field)
151 def validate_custom_field(custom_field)
148 []
152 []
149 end
153 end
150
154
151 # Returns the validation error messages for custom_value
155 # Returns the validation error messages for custom_value
152 # Should return an empty array if custom_value is valid
156 # Should return an empty array if custom_value is valid
153 def validate_custom_value(custom_value)
157 def validate_custom_value(custom_value)
154 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
158 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
155 errors = values.map do |value|
159 errors = values.map do |value|
156 validate_single_value(custom_value.custom_field, value, custom_value.customized)
160 validate_single_value(custom_value.custom_field, value, custom_value.customized)
157 end
161 end
158 errors.flatten.uniq
162 errors.flatten.uniq
159 end
163 end
160
164
161 def validate_single_value(custom_field, value, customized=nil)
165 def validate_single_value(custom_field, value, customized=nil)
162 []
166 []
163 end
167 end
164
168
165 def formatted_custom_value(view, custom_value, html=false)
169 def formatted_custom_value(view, custom_value, html=false)
166 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
170 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
167 end
171 end
168
172
169 def formatted_value(view, custom_field, value, customized=nil, html=false)
173 def formatted_value(view, custom_field, value, customized=nil, html=false)
170 casted = cast_value(custom_field, value, customized)
174 casted = cast_value(custom_field, value, customized)
171 if html && custom_field.url_pattern.present?
175 if html && custom_field.url_pattern.present?
172 texts_and_urls = Array.wrap(casted).map do |single_value|
176 texts_and_urls = Array.wrap(casted).map do |single_value|
173 text = view.format_object(single_value, false).to_s
177 text = view.format_object(single_value, false).to_s
174 url = url_from_pattern(custom_field, single_value, customized)
178 url = url_from_pattern(custom_field, single_value, customized)
175 [text, url]
179 [text, url]
176 end
180 end
177 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
181 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
178 links.join(', ').html_safe
182 links.join(', ').html_safe
179 else
183 else
180 casted
184 casted
181 end
185 end
182 end
186 end
183
187
184 # Returns an URL generated with the custom field URL pattern
188 # Returns an URL generated with the custom field URL pattern
185 # and variables substitution:
189 # and variables substitution:
186 # %value% => the custom field value
190 # %value% => the custom field value
187 # %id% => id of the customized object
191 # %id% => id of the customized object
188 # %project_id% => id of the project of the customized object if defined
192 # %project_id% => id of the project of the customized object if defined
189 # %project_identifier% => identifier of the project of the customized object if defined
193 # %project_identifier% => identifier of the project of the customized object if defined
190 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
194 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
191 def url_from_pattern(custom_field, value, customized)
195 def url_from_pattern(custom_field, value, customized)
192 url = custom_field.url_pattern.to_s.dup
196 url = custom_field.url_pattern.to_s.dup
193 url.gsub!('%value%') {value.to_s}
197 url.gsub!('%value%') {value.to_s}
194 url.gsub!('%id%') {customized.id.to_s}
198 url.gsub!('%id%') {customized.id.to_s}
195 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
199 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
196 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
200 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
197 if custom_field.regexp.present?
201 if custom_field.regexp.present?
198 url.gsub!(%r{%m(\d+)%}) do
202 url.gsub!(%r{%m(\d+)%}) do
199 m = $1.to_i
203 m = $1.to_i
200 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
204 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
201 matches[m].to_s
205 matches[m].to_s
202 end
206 end
203 end
207 end
204 end
208 end
205 url
209 url
206 end
210 end
207 protected :url_from_pattern
211 protected :url_from_pattern
208
212
209 def edit_tag(view, tag_id, tag_name, custom_value, options={})
213 def edit_tag(view, tag_id, tag_name, custom_value, options={})
210 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
214 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
211 end
215 end
212
216
213 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
217 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
214 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
218 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
215 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
219 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
216 end
220 end
217
221
218 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
222 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
219 if custom_field.is_required?
223 if custom_field.is_required?
220 ''.html_safe
224 ''.html_safe
221 else
225 else
222 view.content_tag('label',
226 view.content_tag('label',
223 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
227 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
224 :class => 'inline'
228 :class => 'inline'
225 )
229 )
226 end
230 end
227 end
231 end
228 protected :bulk_clear_tag
232 protected :bulk_clear_tag
229
233
230 def query_filter_options(custom_field, query)
234 def query_filter_options(custom_field, query)
231 {:type => :string}
235 {:type => :string}
232 end
236 end
233
237
234 def before_custom_field_save(custom_field)
238 def before_custom_field_save(custom_field)
235 end
239 end
236
240
237 # Returns a ORDER BY clause that can used to sort customized
241 # Returns a ORDER BY clause that can used to sort customized
238 # objects by their value of the custom field.
242 # objects by their value of the custom field.
239 # Returns nil if the custom field can not be used for sorting.
243 # Returns nil if the custom field can not be used for sorting.
240 def order_statement(custom_field)
244 def order_statement(custom_field)
241 # COALESCE is here to make sure that blank and NULL values are sorted equally
245 # COALESCE is here to make sure that blank and NULL values are sorted equally
242 "COALESCE(#{join_alias custom_field}.value, '')"
246 "COALESCE(#{join_alias custom_field}.value, '')"
243 end
247 end
244
248
245 # Returns a GROUP BY clause that can used to group by custom value
249 # Returns a GROUP BY clause that can used to group by custom value
246 # Returns nil if the custom field can not be used for grouping.
250 # Returns nil if the custom field can not be used for grouping.
247 def group_statement(custom_field)
251 def group_statement(custom_field)
248 nil
252 nil
249 end
253 end
250
254
251 # Returns a JOIN clause that is added to the query when sorting by custom values
255 # Returns a JOIN clause that is added to the query when sorting by custom values
252 def join_for_order_statement(custom_field)
256 def join_for_order_statement(custom_field)
253 alias_name = join_alias(custom_field)
257 alias_name = join_alias(custom_field)
254
258
255 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
259 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
256 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
260 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
257 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
261 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
258 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
262 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
259 " AND (#{custom_field.visibility_by_project_condition})" +
263 " AND (#{custom_field.visibility_by_project_condition})" +
260 " AND #{alias_name}.value <> ''" +
264 " AND #{alias_name}.value <> ''" +
261 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
265 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
262 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
266 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
263 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
267 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
264 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
268 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
265 end
269 end
266
270
267 def join_alias(custom_field)
271 def join_alias(custom_field)
268 "cf_#{custom_field.id}"
272 "cf_#{custom_field.id}"
269 end
273 end
270 protected :join_alias
274 protected :join_alias
271 end
275 end
272
276
273 class Unbounded < Base
277 class Unbounded < Base
274 def validate_single_value(custom_field, value, customized=nil)
278 def validate_single_value(custom_field, value, customized=nil)
275 errs = super
279 errs = super
276 value = value.to_s
280 value = value.to_s
277 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
281 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
278 errs << ::I18n.t('activerecord.errors.messages.invalid')
282 errs << ::I18n.t('activerecord.errors.messages.invalid')
279 end
283 end
280 if custom_field.min_length && value.length < custom_field.min_length
284 if custom_field.min_length && value.length < custom_field.min_length
281 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
285 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
282 end
286 end
283 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
287 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
284 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
288 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
285 end
289 end
286 errs
290 errs
287 end
291 end
288 end
292 end
289
293
290 class StringFormat < Unbounded
294 class StringFormat < Unbounded
291 add 'string'
295 add 'string'
292 self.searchable_supported = true
296 self.searchable_supported = true
293 self.form_partial = 'custom_fields/formats/string'
297 self.form_partial = 'custom_fields/formats/string'
294 field_attributes :text_formatting
298 field_attributes :text_formatting
295
299
296 def formatted_value(view, custom_field, value, customized=nil, html=false)
300 def formatted_value(view, custom_field, value, customized=nil, html=false)
297 if html
301 if html
298 if custom_field.url_pattern.present?
302 if custom_field.url_pattern.present?
299 super
303 super
300 elsif custom_field.text_formatting == 'full'
304 elsif custom_field.text_formatting == 'full'
301 view.textilizable(value, :object => customized)
305 view.textilizable(value, :object => customized)
302 else
306 else
303 value.to_s
307 value.to_s
304 end
308 end
305 else
309 else
306 value.to_s
310 value.to_s
307 end
311 end
308 end
312 end
309 end
313 end
310
314
311 class TextFormat < Unbounded
315 class TextFormat < Unbounded
312 add 'text'
316 add 'text'
313 self.searchable_supported = true
317 self.searchable_supported = true
314 self.form_partial = 'custom_fields/formats/text'
318 self.form_partial = 'custom_fields/formats/text'
315 self.change_as_diff = true
319 self.change_as_diff = true
316
320
317 def formatted_value(view, custom_field, value, customized=nil, html=false)
321 def formatted_value(view, custom_field, value, customized=nil, html=false)
318 if html
322 if html
319 if value.present?
323 if value.present?
320 if custom_field.text_formatting == 'full'
324 if custom_field.text_formatting == 'full'
321 view.textilizable(value, :object => customized)
325 view.textilizable(value, :object => customized)
322 else
326 else
323 view.simple_format(html_escape(value))
327 view.simple_format(html_escape(value))
324 end
328 end
325 else
329 else
326 ''
330 ''
327 end
331 end
328 else
332 else
329 value.to_s
333 value.to_s
330 end
334 end
331 end
335 end
332
336
333 def edit_tag(view, tag_id, tag_name, custom_value, options={})
337 def edit_tag(view, tag_id, tag_name, custom_value, options={})
334 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
338 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
335 end
339 end
336
340
337 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
341 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
338 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
342 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
339 '<br />'.html_safe +
343 '<br />'.html_safe +
340 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
344 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
341 end
345 end
342
346
343 def query_filter_options(custom_field, query)
347 def query_filter_options(custom_field, query)
344 {:type => :text}
348 {:type => :text}
345 end
349 end
346 end
350 end
347
351
348 class LinkFormat < StringFormat
352 class LinkFormat < StringFormat
349 add 'link'
353 add 'link'
350 self.searchable_supported = false
354 self.searchable_supported = false
351 self.form_partial = 'custom_fields/formats/link'
355 self.form_partial = 'custom_fields/formats/link'
352
356
353 def formatted_value(view, custom_field, value, customized=nil, html=false)
357 def formatted_value(view, custom_field, value, customized=nil, html=false)
354 if html
358 if html
355 if custom_field.url_pattern.present?
359 if custom_field.url_pattern.present?
356 url = url_from_pattern(custom_field, value, customized)
360 url = url_from_pattern(custom_field, value, customized)
357 else
361 else
358 url = value.to_s
362 url = value.to_s
359 unless url =~ %r{\A[a-z]+://}i
363 unless url =~ %r{\A[a-z]+://}i
360 # no protocol found, use http by default
364 # no protocol found, use http by default
361 url = "http://" + url
365 url = "http://" + url
362 end
366 end
363 end
367 end
364 view.link_to value.to_s.truncate(40), url
368 view.link_to value.to_s.truncate(40), url
365 else
369 else
366 value.to_s
370 value.to_s
367 end
371 end
368 end
372 end
369 end
373 end
370
374
371 class Numeric < Unbounded
375 class Numeric < Unbounded
372 self.form_partial = 'custom_fields/formats/numeric'
376 self.form_partial = 'custom_fields/formats/numeric'
377 self.totalable_supported = true
373
378
374 def order_statement(custom_field)
379 def order_statement(custom_field)
375 # Make the database cast values into numeric
380 # Make the database cast values into numeric
376 # Postgresql will raise an error if a value can not be casted!
381 # Postgresql will raise an error if a value can not be casted!
377 # CustomValue validations should ensure that it doesn't occur
382 # CustomValue validations should ensure that it doesn't occur
378 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
383 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
379 end
384 end
385
386 # Returns totals for the given scope
387 def total_for_scope(custom_field, scope)
388 scope.joins(:custom_values).
389 where(:custom_values => {:custom_field_id => custom_field.id}).
390 where.not(:custom_values => {:value => ''}).
391 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
392 end
393
394 def cast_total_value(custom_field, value)
395 cast_single_value(custom_field, value)
396 end
380 end
397 end
381
398
382 class IntFormat < Numeric
399 class IntFormat < Numeric
383 add 'int'
400 add 'int'
384
401
385 def label
402 def label
386 "label_integer"
403 "label_integer"
387 end
404 end
388
405
389 def cast_single_value(custom_field, value, customized=nil)
406 def cast_single_value(custom_field, value, customized=nil)
390 value.to_i
407 value.to_i
391 end
408 end
392
409
393 def validate_single_value(custom_field, value, customized=nil)
410 def validate_single_value(custom_field, value, customized=nil)
394 errs = super
411 errs = super
395 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
412 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
396 errs
413 errs
397 end
414 end
398
415
399 def query_filter_options(custom_field, query)
416 def query_filter_options(custom_field, query)
400 {:type => :integer}
417 {:type => :integer}
401 end
418 end
402
419
403 def group_statement(custom_field)
420 def group_statement(custom_field)
404 order_statement(custom_field)
421 order_statement(custom_field)
405 end
422 end
406 end
423 end
407
424
408 class FloatFormat < Numeric
425 class FloatFormat < Numeric
409 add 'float'
426 add 'float'
410
427
411 def cast_single_value(custom_field, value, customized=nil)
428 def cast_single_value(custom_field, value, customized=nil)
412 value.to_f
429 value.to_f
413 end
430 end
414
431
432 def cast_total_value(custom_field, value)
433 value.to_f.round(2)
434 end
435
415 def validate_single_value(custom_field, value, customized=nil)
436 def validate_single_value(custom_field, value, customized=nil)
416 errs = super
437 errs = super
417 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
438 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
418 errs
439 errs
419 end
440 end
420
441
421 def query_filter_options(custom_field, query)
442 def query_filter_options(custom_field, query)
422 {:type => :float}
443 {:type => :float}
423 end
444 end
424 end
445 end
425
446
426 class DateFormat < Unbounded
447 class DateFormat < Unbounded
427 add 'date'
448 add 'date'
428 self.form_partial = 'custom_fields/formats/date'
449 self.form_partial = 'custom_fields/formats/date'
429
450
430 def cast_single_value(custom_field, value, customized=nil)
451 def cast_single_value(custom_field, value, customized=nil)
431 value.to_date rescue nil
452 value.to_date rescue nil
432 end
453 end
433
454
434 def validate_single_value(custom_field, value, customized=nil)
455 def validate_single_value(custom_field, value, customized=nil)
435 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
456 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
436 []
457 []
437 else
458 else
438 [::I18n.t('activerecord.errors.messages.not_a_date')]
459 [::I18n.t('activerecord.errors.messages.not_a_date')]
439 end
460 end
440 end
461 end
441
462
442 def edit_tag(view, tag_id, tag_name, custom_value, options={})
463 def edit_tag(view, tag_id, tag_name, custom_value, options={})
443 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
464 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
444 view.calendar_for(tag_id)
465 view.calendar_for(tag_id)
445 end
466 end
446
467
447 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
468 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
448 view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
469 view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
449 view.calendar_for(tag_id) +
470 view.calendar_for(tag_id) +
450 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
471 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
451 end
472 end
452
473
453 def query_filter_options(custom_field, query)
474 def query_filter_options(custom_field, query)
454 {:type => :date}
475 {:type => :date}
455 end
476 end
456
477
457 def group_statement(custom_field)
478 def group_statement(custom_field)
458 order_statement(custom_field)
479 order_statement(custom_field)
459 end
480 end
460 end
481 end
461
482
462 class List < Base
483 class List < Base
463 self.multiple_supported = true
484 self.multiple_supported = true
464 field_attributes :edit_tag_style
485 field_attributes :edit_tag_style
465
486
466 def edit_tag(view, tag_id, tag_name, custom_value, options={})
487 def edit_tag(view, tag_id, tag_name, custom_value, options={})
467 if custom_value.custom_field.edit_tag_style == 'check_box'
488 if custom_value.custom_field.edit_tag_style == 'check_box'
468 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
489 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
469 else
490 else
470 select_edit_tag(view, tag_id, tag_name, custom_value, options)
491 select_edit_tag(view, tag_id, tag_name, custom_value, options)
471 end
492 end
472 end
493 end
473
494
474 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
495 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
475 opts = []
496 opts = []
476 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
497 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
477 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
498 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
478 opts += possible_values_options(custom_field, objects)
499 opts += possible_values_options(custom_field, objects)
479 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
500 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
480 end
501 end
481
502
482 def query_filter_options(custom_field, query)
503 def query_filter_options(custom_field, query)
483 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
504 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
484 end
505 end
485
506
486 protected
507 protected
487
508
488 # Returns the values that are available in the field filter
509 # Returns the values that are available in the field filter
489 def query_filter_values(custom_field, query)
510 def query_filter_values(custom_field, query)
490 possible_values_options(custom_field, query.project)
511 possible_values_options(custom_field, query.project)
491 end
512 end
492
513
493 # Renders the edit tag as a select tag
514 # Renders the edit tag as a select tag
494 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
515 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
495 blank_option = ''.html_safe
516 blank_option = ''.html_safe
496 unless custom_value.custom_field.multiple?
517 unless custom_value.custom_field.multiple?
497 if custom_value.custom_field.is_required?
518 if custom_value.custom_field.is_required?
498 unless custom_value.custom_field.default_value.present?
519 unless custom_value.custom_field.default_value.present?
499 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
520 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
500 end
521 end
501 else
522 else
502 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
523 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
503 end
524 end
504 end
525 end
505 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
526 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
506 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
527 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
507 if custom_value.custom_field.multiple?
528 if custom_value.custom_field.multiple?
508 s << view.hidden_field_tag(tag_name, '')
529 s << view.hidden_field_tag(tag_name, '')
509 end
530 end
510 s
531 s
511 end
532 end
512
533
513 # Renders the edit tag as check box or radio tags
534 # Renders the edit tag as check box or radio tags
514 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
535 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
515 opts = []
536 opts = []
516 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
537 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
517 opts << ["(#{l(:label_none)})", '']
538 opts << ["(#{l(:label_none)})", '']
518 end
539 end
519 opts += possible_custom_value_options(custom_value)
540 opts += possible_custom_value_options(custom_value)
520 s = ''.html_safe
541 s = ''.html_safe
521 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
542 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
522 opts.each do |label, value|
543 opts.each do |label, value|
523 value ||= label
544 value ||= label
524 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
545 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
525 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
546 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
526 # set the id on the first tag only
547 # set the id on the first tag only
527 tag_id = nil
548 tag_id = nil
528 s << view.content_tag('label', tag + ' ' + label)
549 s << view.content_tag('label', tag + ' ' + label)
529 end
550 end
530 if custom_value.custom_field.multiple?
551 if custom_value.custom_field.multiple?
531 s << view.hidden_field_tag(tag_name, '')
552 s << view.hidden_field_tag(tag_name, '')
532 end
553 end
533 css = "#{options[:class]} check_box_group"
554 css = "#{options[:class]} check_box_group"
534 view.content_tag('span', s, options.merge(:class => css))
555 view.content_tag('span', s, options.merge(:class => css))
535 end
556 end
536 end
557 end
537
558
538 class ListFormat < List
559 class ListFormat < List
539 add 'list'
560 add 'list'
540 self.searchable_supported = true
561 self.searchable_supported = true
541 self.form_partial = 'custom_fields/formats/list'
562 self.form_partial = 'custom_fields/formats/list'
542
563
543 def possible_custom_value_options(custom_value)
564 def possible_custom_value_options(custom_value)
544 options = possible_values_options(custom_value.custom_field)
565 options = possible_values_options(custom_value.custom_field)
545 missing = [custom_value.value].flatten.reject(&:blank?) - options
566 missing = [custom_value.value].flatten.reject(&:blank?) - options
546 if missing.any?
567 if missing.any?
547 options += missing
568 options += missing
548 end
569 end
549 options
570 options
550 end
571 end
551
572
552 def possible_values_options(custom_field, object=nil)
573 def possible_values_options(custom_field, object=nil)
553 custom_field.possible_values
574 custom_field.possible_values
554 end
575 end
555
576
556 def validate_custom_field(custom_field)
577 def validate_custom_field(custom_field)
557 errors = []
578 errors = []
558 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
579 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
559 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
580 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
560 errors
581 errors
561 end
582 end
562
583
563 def validate_custom_value(custom_value)
584 def validate_custom_value(custom_value)
564 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
585 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
565 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
586 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
566 if invalid_values.any?
587 if invalid_values.any?
567 [::I18n.t('activerecord.errors.messages.inclusion')]
588 [::I18n.t('activerecord.errors.messages.inclusion')]
568 else
589 else
569 []
590 []
570 end
591 end
571 end
592 end
572
593
573 def group_statement(custom_field)
594 def group_statement(custom_field)
574 order_statement(custom_field)
595 order_statement(custom_field)
575 end
596 end
576 end
597 end
577
598
578 class BoolFormat < List
599 class BoolFormat < List
579 add 'bool'
600 add 'bool'
580 self.multiple_supported = false
601 self.multiple_supported = false
581 self.form_partial = 'custom_fields/formats/bool'
602 self.form_partial = 'custom_fields/formats/bool'
582
603
583 def label
604 def label
584 "label_boolean"
605 "label_boolean"
585 end
606 end
586
607
587 def cast_single_value(custom_field, value, customized=nil)
608 def cast_single_value(custom_field, value, customized=nil)
588 value == '1' ? true : false
609 value == '1' ? true : false
589 end
610 end
590
611
591 def possible_values_options(custom_field, object=nil)
612 def possible_values_options(custom_field, object=nil)
592 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
613 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
593 end
614 end
594
615
595 def group_statement(custom_field)
616 def group_statement(custom_field)
596 order_statement(custom_field)
617 order_statement(custom_field)
597 end
618 end
598
619
599 def edit_tag(view, tag_id, tag_name, custom_value, options={})
620 def edit_tag(view, tag_id, tag_name, custom_value, options={})
600 case custom_value.custom_field.edit_tag_style
621 case custom_value.custom_field.edit_tag_style
601 when 'check_box'
622 when 'check_box'
602 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
623 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
603 when 'radio'
624 when 'radio'
604 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
625 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
605 else
626 else
606 select_edit_tag(view, tag_id, tag_name, custom_value, options)
627 select_edit_tag(view, tag_id, tag_name, custom_value, options)
607 end
628 end
608 end
629 end
609
630
610 # Renders the edit tag as a simple check box
631 # Renders the edit tag as a simple check box
611 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
632 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
612 s = ''.html_safe
633 s = ''.html_safe
613 s << view.hidden_field_tag(tag_name, '0', :id => nil)
634 s << view.hidden_field_tag(tag_name, '0', :id => nil)
614 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
635 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
615 view.content_tag('span', s, options)
636 view.content_tag('span', s, options)
616 end
637 end
617 end
638 end
618
639
619 class RecordList < List
640 class RecordList < List
620 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
641 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
621
642
622 def cast_single_value(custom_field, value, customized=nil)
643 def cast_single_value(custom_field, value, customized=nil)
623 target_class.find_by_id(value.to_i) if value.present?
644 target_class.find_by_id(value.to_i) if value.present?
624 end
645 end
625
646
626 def target_class
647 def target_class
627 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
648 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
628 end
649 end
629
650
630 def reset_target_class
651 def reset_target_class
631 @target_class = nil
652 @target_class = nil
632 end
653 end
633
654
634 def possible_custom_value_options(custom_value)
655 def possible_custom_value_options(custom_value)
635 options = possible_values_options(custom_value.custom_field, custom_value.customized)
656 options = possible_values_options(custom_value.custom_field, custom_value.customized)
636 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
657 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
637 if missing.any?
658 if missing.any?
638 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
659 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
639 end
660 end
640 options
661 options
641 end
662 end
642
663
643 def order_statement(custom_field)
664 def order_statement(custom_field)
644 if target_class.respond_to?(:fields_for_order_statement)
665 if target_class.respond_to?(:fields_for_order_statement)
645 target_class.fields_for_order_statement(value_join_alias(custom_field))
666 target_class.fields_for_order_statement(value_join_alias(custom_field))
646 end
667 end
647 end
668 end
648
669
649 def group_statement(custom_field)
670 def group_statement(custom_field)
650 "COALESCE(#{join_alias custom_field}.value, '')"
671 "COALESCE(#{join_alias custom_field}.value, '')"
651 end
672 end
652
673
653 def join_for_order_statement(custom_field)
674 def join_for_order_statement(custom_field)
654 alias_name = join_alias(custom_field)
675 alias_name = join_alias(custom_field)
655
676
656 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
677 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
657 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
678 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
658 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
679 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
659 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
680 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
660 " AND (#{custom_field.visibility_by_project_condition})" +
681 " AND (#{custom_field.visibility_by_project_condition})" +
661 " AND #{alias_name}.value <> ''" +
682 " AND #{alias_name}.value <> ''" +
662 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
683 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
663 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
684 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
664 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
685 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
665 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
686 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
666 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
687 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
667 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
688 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
668 end
689 end
669
690
670 def value_join_alias(custom_field)
691 def value_join_alias(custom_field)
671 join_alias(custom_field) + "_" + custom_field.field_format
692 join_alias(custom_field) + "_" + custom_field.field_format
672 end
693 end
673 protected :value_join_alias
694 protected :value_join_alias
674 end
695 end
675
696
676 class EnumerationFormat < RecordList
697 class EnumerationFormat < RecordList
677 add 'enumeration'
698 add 'enumeration'
678 self.form_partial = 'custom_fields/formats/enumeration'
699 self.form_partial = 'custom_fields/formats/enumeration'
679
700
680 def label
701 def label
681 "label_field_format_enumeration"
702 "label_field_format_enumeration"
682 end
703 end
683
704
684 def target_class
705 def target_class
685 @target_class ||= CustomFieldEnumeration
706 @target_class ||= CustomFieldEnumeration
686 end
707 end
687
708
688 def possible_values_options(custom_field, object=nil)
709 def possible_values_options(custom_field, object=nil)
689 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
710 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
690 end
711 end
691
712
692 def possible_values_records(custom_field, object=nil)
713 def possible_values_records(custom_field, object=nil)
693 custom_field.enumerations.active
714 custom_field.enumerations.active
694 end
715 end
695
716
696 def value_from_keyword(custom_field, keyword, object)
717 def value_from_keyword(custom_field, keyword, object)
697 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword)
718 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword)
698 value ? value.id : nil
719 value ? value.id : nil
699 end
720 end
700 end
721 end
701
722
702 class UserFormat < RecordList
723 class UserFormat < RecordList
703 add 'user'
724 add 'user'
704 self.form_partial = 'custom_fields/formats/user'
725 self.form_partial = 'custom_fields/formats/user'
705 field_attributes :user_role
726 field_attributes :user_role
706
727
707 def possible_values_options(custom_field, object=nil)
728 def possible_values_options(custom_field, object=nil)
708 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
729 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
709 end
730 end
710
731
711 def possible_values_records(custom_field, object=nil)
732 def possible_values_records(custom_field, object=nil)
712 if object.is_a?(Array)
733 if object.is_a?(Array)
713 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
734 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
714 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
735 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
715 elsif object.respond_to?(:project) && object.project
736 elsif object.respond_to?(:project) && object.project
716 scope = object.project.users
737 scope = object.project.users
717 if custom_field.user_role.is_a?(Array)
738 if custom_field.user_role.is_a?(Array)
718 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
739 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
719 if role_ids.any?
740 if role_ids.any?
720 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
741 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
721 end
742 end
722 end
743 end
723 scope.sorted
744 scope.sorted
724 else
745 else
725 []
746 []
726 end
747 end
727 end
748 end
728
749
729 def value_from_keyword(custom_field, keyword, object)
750 def value_from_keyword(custom_field, keyword, object)
730 users = possible_values_records(custom_field, object).to_a
751 users = possible_values_records(custom_field, object).to_a
731 user = Principal.detect_by_keyword(users, keyword)
752 user = Principal.detect_by_keyword(users, keyword)
732 user ? user.id : nil
753 user ? user.id : nil
733 end
754 end
734
755
735 def before_custom_field_save(custom_field)
756 def before_custom_field_save(custom_field)
736 super
757 super
737 if custom_field.user_role.is_a?(Array)
758 if custom_field.user_role.is_a?(Array)
738 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
759 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
739 end
760 end
740 end
761 end
741 end
762 end
742
763
743 class VersionFormat < RecordList
764 class VersionFormat < RecordList
744 add 'version'
765 add 'version'
745 self.form_partial = 'custom_fields/formats/version'
766 self.form_partial = 'custom_fields/formats/version'
746 field_attributes :version_status
767 field_attributes :version_status
747
768
748 def possible_values_options(custom_field, object=nil)
769 def possible_values_options(custom_field, object=nil)
749 versions_options(custom_field, object)
770 versions_options(custom_field, object)
750 end
771 end
751
772
752 def before_custom_field_save(custom_field)
773 def before_custom_field_save(custom_field)
753 super
774 super
754 if custom_field.version_status.is_a?(Array)
775 if custom_field.version_status.is_a?(Array)
755 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
776 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
756 end
777 end
757 end
778 end
758
779
759 protected
780 protected
760
781
761 def query_filter_values(custom_field, query)
782 def query_filter_values(custom_field, query)
762 versions_options(custom_field, query.project, true)
783 versions_options(custom_field, query.project, true)
763 end
784 end
764
785
765 def versions_options(custom_field, object, all_statuses=false)
786 def versions_options(custom_field, object, all_statuses=false)
766 if object.is_a?(Array)
787 if object.is_a?(Array)
767 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
788 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
768 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
789 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
769 elsif object.respond_to?(:project) && object.project
790 elsif object.respond_to?(:project) && object.project
770 scope = object.project.shared_versions
791 scope = object.project.shared_versions
771 if !all_statuses && custom_field.version_status.is_a?(Array)
792 if !all_statuses && custom_field.version_status.is_a?(Array)
772 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
793 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
773 if statuses.any?
794 if statuses.any?
774 scope = scope.where(:status => statuses.map(&:to_s))
795 scope = scope.where(:status => statuses.map(&:to_s))
775 end
796 end
776 end
797 end
777 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
798 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
778 else
799 else
779 []
800 []
780 end
801 end
781 end
802 end
782 end
803 end
783 end
804 end
784 end
805 end
General Comments 0
You need to be logged in to leave comments. Login now