Module: HasCustomFields

Extended by:
ActiveSupport::Concern
Included in:
Category, Group, Post, Topic, User
Defined in:
app/models/concerns/has_custom_fields.rb

Defined Under Namespace

Modules: Helpers Classes: FieldDescriptor, NotPreloadedError, PreloadedProxy

Constant Summary collapse

DEFAULT_FIELD_DESCRIPTOR =
FieldDescriptor.new(:string, 10_000_000)
CUSTOM_FIELDS_MAX_ITEMS =
100

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#preloaded_custom_fieldsObject (readonly)

Returns the value of attribute preloaded_custom_fields.



192
193
194
# File 'app/models/concerns/has_custom_fields.rb', line 192

def preloaded_custom_fields
  @preloaded_custom_fields
end

Instance Method Details

#clear_custom_fieldsObject



216
217
218
219
# File 'app/models/concerns/has_custom_fields.rb', line 216

def clear_custom_fields
  @custom_fields = nil
  @custom_fields_orig = nil
end

#create_singular(name, value, field_type = nil) ⇒ Object

We support unique indexes on certain fields. In the event two concurrent processes attempt to update the same custom field we should catch the error and perform an update instead.



334
335
336
337
338
339
340
341
342
343
344
# File 'app/models/concerns/has_custom_fields.rb', line 334

def create_singular(name, value, field_type = nil)
  write_value = value.is_a?(Hash) || field_type == :json ? value.to_json : value
  write_value = "t" if write_value.is_a?(TrueClass)
  write_value = "f" if write_value.is_a?(FalseClass)
  row_count = DB.exec(<<~SQL, name: name, value: write_value, id: id, now: Time.zone.now)
    INSERT INTO #{_custom_fields.table_name} (#{custom_fields_fk}, name, value, created_at, updated_at)
    VALUES (:id, :name, :value, :now, :now)
    ON CONFLICT DO NOTHING
  SQL
  _custom_fields.where(name: name).update_all(value: write_value) if row_count == 0
end

#custom_field_preloaded?(name) ⇒ Boolean

Returns:

  • (Boolean)


212
213
214
# File 'app/models/concerns/has_custom_fields.rb', line 212

def custom_field_preloaded?(name)
  @preloaded_custom_fields && @preloaded_custom_fields.key?(name)
end

#custom_fieldsObject



250
251
252
253
254
255
256
# File 'app/models/concerns/has_custom_fields.rb', line 250

def custom_fields
  if @preloaded_custom_fields
    return @preloaded_proxy ||= PreloadedProxy.new(@preloaded_custom_fields, self.class.to_s)
  end

  @custom_fields ||= refresh_custom_fields_from_db.dup
end

#custom_fields=(data) ⇒ Object



258
259
260
# File 'app/models/concerns/has_custom_fields.rb', line 258

def custom_fields=(data)
  custom_fields.replace(data)
end

#custom_fields_clean?Boolean

Returns:

  • (Boolean)


262
263
264
265
# File 'app/models/concerns/has_custom_fields.rb', line 262

def custom_fields_clean?
  # Check whether the cached version has been changed on this model
  !@custom_fields || @custom_fields_orig == @custom_fields
end

#custom_fields_fkObject



194
195
196
# File 'app/models/concerns/has_custom_fields.rb', line 194

def custom_fields_fk
  @custom_fields_fk ||= "#{_custom_fields.reflect_on_all_associations(:belongs_to)[0].name}_id"
end

#custom_fields_preloaded?Boolean

Returns:

  • (Boolean)


208
209
210
# File 'app/models/concerns/has_custom_fields.rb', line 208

def custom_fields_preloaded?
  !!@preloaded_custom_fields
end

#on_custom_fields_changeObject



203
204
205
206
# File 'app/models/concerns/has_custom_fields.rb', line 203

def on_custom_fields_change
  # Callback when custom fields have changed
  # Override in model
end

#reload(options = nil) ⇒ Object



198
199
200
201
# File 'app/models/concerns/has_custom_fields.rb', line 198

def reload(options = nil)
  clear_custom_fields
  super
end

#save_custom_fields(force = false, run_validations: true) ⇒ Object



282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'app/models/concerns/has_custom_fields.rb', line 282

def save_custom_fields(force = false, run_validations: true)
  if run_validations
    custom_fields_max_items
    custom_fields_value_length
    raise_validation_error unless errors.empty?
  end

  if force || !custom_fields_clean?
    ActiveRecord::Base.transaction do
      dup = @custom_fields.dup.with_indifferent_access
      fields_by_key = _custom_fields.reload.group_by(&:name)

      (dup.keys.to_set + fields_by_key.keys.to_set).each do |key|
        fields = fields_by_key[key] || []
        value = dup[key]
        descriptor = self.class.get_custom_field_descriptor(key)
        field_type = descriptor.type

        if descriptor.array_type? || (field_type != :json && Array === value)
          value = Array(value || [])
          value.compact!
          sub_type = field_type[0]

          value.map! { |v| descriptor.serialize(v) }

          unless value == fields.map(&:value)
            fields.each(&:destroy!)

            value.each { |subv| _custom_fields.create!(name: key, value: subv) }
          end
        else
          if value.nil?
            fields.each(&:destroy!)
          else
            value = descriptor.serialize(value)

            field = fields.find { |f| f.value == value }
            fields.select { |f| f != field }.each(&:destroy!)

            create_singular(key, value) if !field
          end
        end
      end
    end

    on_custom_fields_change
    refresh_custom_fields_from_db
  end
end

#set_preloaded_custom_fields(custom_fields) ⇒ Object



241
242
243
244
245
246
247
248
# File 'app/models/concerns/has_custom_fields.rb', line 241

def set_preloaded_custom_fields(custom_fields)
  @preloaded_custom_fields = custom_fields

  # we have to clear this otherwise the fields are cached inside the
  # already existing proxy and no new ones are added, so when we check
  # for custom_fields[KEY] an error is likely to occur
  @preloaded_proxy = nil
end

#upsert_custom_fields(fields) ⇒ Object

‘upsert_custom_fields` will only insert/update existing fields, and will not delete anything. It is safer under concurrency and is recommended when you just want to attach fields to things without maintaining a specific set of fields.



271
272
273
274
275
276
277
278
279
280
# File 'app/models/concerns/has_custom_fields.rb', line 271

def upsert_custom_fields(fields)
  fields.each do |k, v|
    row_count = _custom_fields.where(name: k).update_all(value: v, updated_at: Time.now)
    _custom_fields.create!(name: k, value: v) if row_count == 0

    custom_fields[k.to_s] = v # We normalize custom_fields as strings
  end

  on_custom_fields_change
end