custom_field.rb 9.09 KB
Newer Older
1
# Redmine - project management software
jplang's avatar
jplang committed
2
# Copyright (C) 2006-2017  Jean-Philippe Lang
3
4
5
6
7
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
8
#
9
10
11
12
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
13
#
14
15
16
17
18
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

class CustomField < ActiveRecord::Base
19
  include Redmine::SafeAttributes
20
21
  include Redmine::SubclassFactory

22
23
24
25
  has_many :enumerations,
           lambda { order(:position) },
           :class_name => 'CustomFieldEnumeration',
           :dependent => :delete_all
26
  has_many :custom_values, :dependent => :delete_all
27
  has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
28
  acts_as_positioned
29
  serialize :possible_values
30
  store :format_store
31

32
  validates_presence_of :name, :field_format
33
  validates_uniqueness_of :name, :scope => :type
jplang's avatar
jplang committed
34
  validates_length_of :name, :maximum => 30
35
  validates_length_of :regexp, maximum: 255
36
  validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
37
  validate :validate_custom_field
38

39
  before_validation :set_searchable
40
41
42
  before_save do |field|
    field.format.before_custom_field_save(field)
  end
43
  after_save :handle_multiplicity_change
44
  after_save do |field|
jplang's avatar
jplang committed
45
    if field.saved_change_to_visible? && field.visible
46
47
48
      field.roles.clear
    end
  end
49

jplang's avatar
jplang committed
50
  scope :sorted, lambda { order(:position) }
51
52
53
54
55
56
57
58
59
60
61
62
63
64
  scope :visible, lambda {|*args|
    user = args.shift || User.current
    if user.admin?
      # nop
    elsif user.memberships.any?
      where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
        " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
        " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
        " WHERE m.user_id = ?)",
        true, user.id)
    else
      where(:visible => true)
    end
  }
65
66
67
68
  def visible_by?(project, user=User.current)
    visible? || user.admin?
  end

69
70
71
72
  safe_attributes 'name',
    'field_format',
    'possible_values',
    'regexp',
73
    'min_length',
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
    'max_length',
    'is_required',
    'is_for_all',
    'is_filter',
    'position',
    'searchable',
    'default_value',
    'editable',
    'visible',
    'multiple',
    'description',
    'role_ids',
    'url_pattern',
    'text_formatting',
    'edit_tag_style',
    'user_role',
90
    'version_status',
91
92
    'extensions_allowed',
    'full_width_layout'
93

94
95
96
97
  def format
    @format ||= Redmine::FieldFormat.find(field_format)
  end

98
99
  def field_format=(arg)
    # cannot change format of a saved custom field
100
101
102
103
    if new_record?
      @format = nil
      super
    end
104
105
  end

106
  def set_searchable
107
    # make sure these fields are not searchable
108
    self.searchable = false unless format.class.searchable_supported
109
    # make sure only these fields can have multiple values
110
    self.multiple = false unless format.class.multiple_supported
111
    true
112
  end
113

114
  def validate_custom_field
115
116
    format.validate_custom_field(self).each do |attribute, message|
      errors.add attribute, message
117
    end
118

jplang's avatar
jplang committed
119
120
121
122
123
124
125
    if regexp.present?
      begin
        Regexp.new(regexp)
      rescue
        errors.add(:regexp, :invalid)
      end
    end
126

127
128
129
130
    if default_value.present?
      validate_field_value(default_value).each do |message|
        errors.add :default_value, message
      end
131
    end
132
  end
133

134
135
136
137
138
139
140
  def possible_custom_value_options(custom_value)
    format.possible_custom_value_options(custom_value)
  end

  def possible_values_options(object=nil)
    if object.is_a?(Array)
      object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
141
    else
142
      format.possible_values_options(self, object) || []
143
144
    end
  end
145

146
  def possible_values
147
    values = read_attribute(:possible_values)
148
149
    if values.is_a?(Array)
      values.each do |value|
150
        value.to_s.force_encoding('UTF-8')
jplang's avatar
jplang committed
151
      end
152
153
154
      values
    else
      []
155
156
    end
  end
157

158
159
160
  # Makes possible_values accept a multiline string
  def possible_values=(arg)
    if arg.is_a?(Array)
161
      values = arg.compact.map {|a| a.to_s.strip}.reject(&:blank?)
162
      write_attribute(:possible_values, values)
163
164
165
166
    else
      self.possible_values = arg.to_s.split(/[\n\r]+/)
    end
  end
167

168
169
170
171
  def set_custom_field_value(custom_field_value, value)
    format.set_custom_field_value(self, custom_field_value, value)
  end

172
  def cast_value(value)
173
    format.cast_value(self, value)
174
  end
175

176
  def value_from_keyword(keyword, customized)
177
    format.value_from_keyword(self, keyword, customized)
178
  end
179

180
181
182
183
184
  # Returns the options hash used to build a query filter for the field
  def query_filter_options(query)
    format.query_filter_options(self, query)
  end

185
186
187
188
  def totalable?
    format.totalable_supported
  end

189
190
191
192
  def full_width_layout?
    full_width_layout == '1'
  end

193
194
  # Returns a ORDER BY clause that can used to sort customized
  # objects by their value of the custom field.
jplang's avatar
jplang committed
195
  # Returns nil if the custom field can not be used for sorting.
196
  def order_statement
197
    return nil if multiple?
198
    format.order_statement(self)
199
  end
200

jplang's avatar
jplang committed
201
202
  # Returns a GROUP BY clause that can used to group by custom value
  # Returns nil if the custom field can not be used for grouping.
203
  def group_statement
jplang's avatar
jplang committed
204
    return nil if multiple?
205
    format.group_statement(self)
206
207
208
  end

  def join_for_order_statement
209
    format.join_for_order_statement(self)
210
211
  end

212
  def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
213
214
215
216
217
218
    if visible? || user.admin?
      "1=1"
    elsif user.anonymous?
      "1=0"
    else
      project_key ||= "#{self.class.customized_class.table_name}.project_id"
219
      id_column ||= id
220
      "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
221
222
        " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
        " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
223
        " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
224
225
226
    end
  end

jplang's avatar
jplang committed
227
228
229
  def <=>(field)
    position <=> field.position
  end
230

231
232
  # Returns the class that values represent
  def value_class
233
    format.target_class if format.respond_to?(:target_class)
234
235
  end

236
237
  def self.customized_class
    self.name =~ /^(.+)CustomField$/
238
    $1.constantize rescue nil
239
  end
240

241
242
  # to move in project_custom_field
  def self.for_all
243
    where(:is_for_all => true).order(:position).to_a
244
  end
245

246
247
248
  def type_name
    nil
  end
249

250
  # Returns the error messages for the given value
251
  # or an empty array if value is a valid value for the custom field
252
253
  def validate_custom_value(custom_value)
    value = custom_value.value
254
255
256
257
258
259
260
261
262
263
264
265
266
267
    errs = format.validate_custom_value(custom_value)

    unless errs.any?
      if value.is_a?(Array)
        if !multiple?
          errs << ::I18n.t('activerecord.errors.messages.invalid')
        end
        if is_required? && value.detect(&:present?).nil?
          errs << ::I18n.t('activerecord.errors.messages.blank')
        end
      else
        if is_required? && value.blank?
          errs << ::I18n.t('activerecord.errors.messages.blank')
        end
268
      end
269
    end
270

271
272
273
    errs
  end

274
275
  # Returns the error messages for the default custom field value
  def validate_field_value(value)
276
    validate_custom_value(CustomFieldValue.new(:custom_field => self, :value => value))
277
278
  end

279
280
281
282
283
  # Returns true if value is a valid value for the custom field
  def valid_field_value?(value)
    validate_field_value(value).empty?
  end

284
285
286
287
  def after_save_custom_value(custom_value)
    format.after_save_custom_value(self, custom_value)
  end

288
289
290
291
  def format_in?(*args)
    args.include?(field_format)
  end

292
293
294
295
296
297
298
299
  def self.human_attribute_name(attribute_key_name, *args)
    attr_name = attribute_key_name.to_s
    if attr_name == 'url_pattern'
      attr_name = "url"
    end
    super(attr_name, *args)
  end

300
301
  protected

302
303
304
  # Removes multiple values for the custom field after setting the multiple attribute to false
  # We kepp the value with the highest id for each customized object
  def handle_multiplicity_change
jplang's avatar
jplang committed
305
    if !new_record? && multiple_before_last_save && !multiple
306
      ids = custom_values.
307
308
        where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
          " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
309
310
311
312
313
314
315
316
          " AND cve.id > #{CustomValue.table_name}.id)").
        pluck(:id)

      if ids.any?
        custom_values.where(:id => ids).delete_all
      end
    end
  end
317
end
318
319

require_dependency 'redmine/field_format'