BindaCMS/binda

View on GitHub
app/models/concerns/binda/fieldable_associations.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Binda
    # Fieldable associations are Binda's core feature. 
    # 
    # They provide classes like `Binda::Component` and `Binda::Board` with a collection of fields 
    #   (texts, assets, dates and so on) to store data in a simple yet powerful way. 
    module FieldableAssociations

        extend ActiveSupport::Concern

        included do 

        include FieldableAssociationHelpers
        
        # Fieldable Associations 
        # 
        # If you add a new field remember to update:
        #   - get_fieldables (see here below)
        #   - get_field_types (see here below)
        #   - component_params (app/controllers/binda/components_controller.rb)

        has_many :texts,         as: :fieldable, dependent: :delete_all
        has_many :strings,       as: :fieldable, dependent: :delete_all
        has_many :dates,         as: :fieldable, dependent: :delete_all
        has_many :galleries,     as: :fieldable, dependent: :delete_all
        has_many :assets,        as: :fieldable, dependent: :delete_all
        has_many :images,        as: :fieldable, dependent: :delete_all
        has_many :videos,        as: :fieldable, dependent: :delete_all
        has_many :audios,        as: :fieldable, dependent: :delete_all
        has_many :radios,        as: :fieldable, dependent: :delete_all 
        has_many :selections,    as: :fieldable, dependent: :delete_all 
        has_many :checkboxes,    as: :fieldable, dependent: :delete_all 
        has_many :svgs,          as: :fieldable, dependent: :delete_all
        # Repeaters need destroy_all, not delete_all
        has_many :repeaters,     as: :fieldable, dependent: :destroy
        has_many :relations,     as: :fieldable, dependent: :destroy 


        has_many :owner_relations, class_name: "RelationLink", 
                                dependent: :destroy, 
                                as: :dependent

        # Owner are connected to its Dependents in a Active Relation
        # meaning its possible to connect a Owner to as many Dependents
        # as it's needed.
        # 
        # The current version support components and boards separately
        has_many :owner_components, through: :owner_relations, 
                                    source: :owner

        has_many :owner_boards, through: :owner_relations, 
                                source: :owner

        has_many :owner_repeaters, through: :owner_relations, 
                                source: :owner 


        accepts_nested_attributes_for :texts, :strings, :dates, :assets, :images, :videos, :audios, :galleries, :repeaters, :radios, :selections, :checkboxes, :relations, :svgs, allow_destroy: true

            validates_associated :texts
            validates_associated :strings
            validates_associated :dates
            validates_associated :assets
            validates_associated :images
            validates_associated :videos
            validates_associated :audios
            validates_associated :repeaters
            validates_associated :radios
            validates_associated :selections
            validates_associated :checkboxes
            validates_associated :relations
            validates_associated :svgs

      after_save :generate_fields

      # Uncomment these "validate do" loop to better debug validation.
      # This makes method gather errors of the associated records and
      # make them available to the instance object. After using this method 
      # you will be able to see the actual error inside `instance.errors` array.
      # Example: @component.errors #=> [ ... ]
      # 
          # validate do |instance|
          #   instance.texts.each do |text|
          #       binding.pry
          #     next if text.valid?
          #     text.errors.full_messages.each do |msg|
          #       # you can customize the error message here:
          #       errors[:base] << "Error in #{text.field_setting.name} (text): #{msg}"
          #     end
          #   end
          # end
        end

        # Find or create a field by field setting and field type
        # 
        # This is used in Binda's editor views.
        #   
        # Please, check the code to know more about the way this method works as it's pretty complex yet important.
        # 
        # @param field_setting_id [string] The field setting id
        # @param field_type [string] THe field type
        def find_or_create_a_field_by(field_setting_id, field_type)
            if FieldSetting.get_field_classes.include?( field_type.classify ) && field_setting_id.is_a?( Integer )
                get_field(field_type, field_setting_id)
            else
                raise ArgumentError, "One parameter in find_or_create_a_field_by() is not correct on instance (#{self.class.name} ##{self.id}).", caller
            end
        end

    # This method is called upon the creation/update of a fieldable record (component, board or repeater) 
    #   and generates all fields related to each field settings which belongs to it.
    # 
    # This avoids any situation in which, for example, a component have a field setting for a text
    #   but there is no text (meaning `Binda::Text` instance) that correspond to that field setting.
    #   This causes issues when looping a bunch of components which will thow a error if you try to access
    #   a component field, as some might have it some might not. This make sure that you can always expect 
    #   to find a field instance which might be empty, but certainly it exists.
    #   
    # TODO check if find_or_create_a_field_by method should be used instead (it's used in editors views)
    # 
    def generate_fields
      GenerateFieldsJob.perform_later self
    end

        # TODO: Update all helpers replacing `find` method with ruby `select`. 
        # This should improve performance avoiding generating useless ActiveRecord objects.

        private

            # Get field based on field type and field setting
            # @param field_type [string]
            # @param field_setting_id [integer]
            # @return [ActiveRecord]
            def get_field(field_type, field_setting_id)
                # It's mandatory to use `select{}.first`!!! 
                # If you use any ActiveRecord method (like `where` of `find`) the validation errors are wiped out 
                # from the object and not rendered next to the form in the editor view
                obj = self.send( field_type.pluralize ).select{|rf| rf.field_setting_id == field_setting_id}.first
                if obj.nil?
                    # As we are using the `select{}.first` method, asynchronous requests passing through 
                    # this method will create some inconsistentcy between this `self` object and the real object. 
                    # In other words `self` should be reloaded, but we won't reload it otherwise we will 
                    # erase the errors shipped with it initially. Therefore we will check again with `find_or_create_by!`.
                    # This leads to another issue, which is: errors coming from asynchronous requests won't be considered
                    # as the `self` object is the initial one, not the "updated" one. At the current time this is not
                    # a problem because the only asynchronous requests are for brand new records which don't need validation.
                    return self.send( field_type.pluralize ).find_or_create_by!( field_setting_id: field_setting_id ) 
                else
                    return obj
                end 
            end
    end
end