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: NotPreloadedError, PreloadedProxy

Constant Summary collapse

CUSTOM_FIELDS_MAX_ITEMS =
100
CUSTOM_FIELDS_MAX_VALUE_LENGTH =
10_000_000

Instance Method Summary collapse

Instance Method Details

#clear_custom_fieldsObject



185
186
187
188
# File 'app/models/concerns/has_custom_fields.rb', line 185

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.



309
310
311
312
313
314
315
316
317
318
319
# File 'app/models/concerns/has_custom_fields.rb', line 309

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)


181
182
183
# File 'app/models/concerns/has_custom_fields.rb', line 181

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

#custom_fieldsObject



218
219
220
221
222
223
224
# File 'app/models/concerns/has_custom_fields.rb', line 218

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



226
227
228
# File 'app/models/concerns/has_custom_fields.rb', line 226

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

#custom_fields_clean?Boolean

Returns:

  • (Boolean)


230
231
232
233
# File 'app/models/concerns/has_custom_fields.rb', line 230

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_preloaded?Boolean

Returns:

  • (Boolean)


177
178
179
# File 'app/models/concerns/has_custom_fields.rb', line 177

def custom_fields_preloaded?
  !!@preloaded_custom_fields
end

#on_custom_fields_changeObject



172
173
174
175
# File 'app/models/concerns/has_custom_fields.rb', line 172

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

#reload(options = nil) ⇒ Object



167
168
169
170
# File 'app/models/concerns/has_custom_fields.rb', line 167

def reload(options = nil)
  clear_custom_fields
  super
end

#save_custom_fields(force = false) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'app/models/concerns/has_custom_fields.rb', line 250

def save_custom_fields(force = false)
  if force || !custom_fields_clean?
    dup = @custom_fields.dup.with_indifferent_access
    array_fields = {}

    ActiveRecord::Base.transaction do
      _custom_fields.reload.each do |f|
        if dup[f.name].is_a?(Array)
          # we need to collect Arrays fully before we can compare them
          if !array_fields.has_key?(f.name)
            array_fields[f.name] = [f]
          else
            array_fields[f.name] << f
          end
        elsif dup[f.name].is_a?(Hash)
          if dup[f.name].to_json != f.value
            f.destroy!
          else
            dup.delete(f.name)
          end
        else
          t = {}
          self.class.append_custom_field(t, f.name, f.value)

          if dup.has_key?(f.name) && dup[f.name] == t[f.name]
            dup.delete(f.name)
          else
            f.destroy!
          end
        end
      end

      # let's iterate through our arrays and compare them
      array_fields.each do |field_name, fields|
        if fields.length == dup[field_name].length && fields.map(&:value) == dup[field_name]
          dup.delete(field_name)
        else
          fields.each(&:destroy!)
        end
      end

      dup.each do |k, v|
        field_type = self.class.get_custom_field_type(k)

        if v.is_a?(Array) && field_type != :json
          v.each { |subv| _custom_fields.create!(name: k, value: subv) }
        else
          create_singular(k, v, field_type)
        end
      end
    end

    on_custom_fields_change
    refresh_custom_fields_from_db
  end
end

#set_preloaded_custom_fields(custom_fields) ⇒ Object



209
210
211
212
213
214
215
216
# File 'app/models/concerns/has_custom_fields.rb', line 209

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.



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

def upsert_custom_fields(fields)
  fields.each do |k, v|
    row_count = _custom_fields.where(name: k).update_all(value: v)
    _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