ministryofjustice/atet

View on GitHub
app/forms/form.rb

Summary

Maintainability
A
0 mins
Test Coverage
class Form < ApplicationRecord
  self.abstract_class = true
  establish_connection adapter: :nulldb,
                       schema: 'config/nulldb_schema.rb'
  attr_reader :resource

  before_validation :clean_strings
  class_attribute :__transient_attributes, default: []
  class_attribute :__custom_mappings, default: {}

  def self.inherited(child)
    child.__transient_attributes = []
    child.__custom_mappings = __custom_mappings.clone
    super
  end

  def initialize(resource, **attrs)
    self.resource = resource
    super(attrs)
    reload
    yield self if block_given?
  end

  def self.transient_attributes(*attrs)
    __transient_attributes.concat attrs unless attrs.empty?
    __transient_attributes
  end

  def self.map_attribute(attribute_name, to:)
    __custom_mappings[attribute_name] = { to: }
  end

  def transient_attributes
    self.class.transient_attributes
  end

  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
  def self.has_many_forms(collection_name, class_name: "#{collection_name.to_s.singularize.camelize}Form")
    class_eval do
      before_save :"update_#{collection_name}"
      after_save :"#{collection_name}_after_save"
      define_method :"#{collection_name}_proxy" do
        existing = instance_variable_get("@#{collection_name}_proxy")
        return existing unless existing.nil?

        instance_variable_set("@#{collection_name}_proxy", FormCollectionProxy.new(class_name, collection_name, self))
      end

      define_method :"#{collection_name}" do
        send("#{collection_name}_proxy")
      end

      define_method :"#{collection_name}_attributes=" do |attrs|
        send("#{collection_name}_proxy").collection_attributes = attrs
      end

      define_method :"update_#{collection_name}" do
        proxy = send :"#{collection_name}"
        target.attributes = {
          "#{collection_name}_attributes": proxy.collection_attributes
        }
      end

      define_method :"#{collection_name}_after_save" do
        proxy = send :"#{collection_name}"
        proxy.after_save
      end
    end
  end
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Naming/PredicateName

  # Form Methods
  def form_name
    self.class.model_name_i18n_key.to_s.dasherize
  end

  def self.model_name_i18n_key
    model_name.i18n_key
  end

  def self.model_name
    ActiveModel::Name.new(self, nil, name.underscore.sub(/_form\Z/, ''))
  end

  def self.for(name)
    "#{name}_form".classify.constantize
  end

  # This is required as the standard implementation checks the connection to see if the table exists
  # but the connection is the null db adapter, which relies on the schema file to define tables.
  # However, we do not want the table 'definitions' to be defined outside of the form objects
  #
  # If this returns false, then methods dont get defined for the attributes so all attribute accesses use method_missing
  # which is slower and also it means you wont be able to do things like have_attributes in rspec on a form without this
  def self.table_exists?
    true
  end

  # target is normally the resource but can be overriden
  # @TODO - do we need to do this ?
  def target
    resource
  end

  # @TODO This is used to provide a boolean which isn't technically
  # an attribute so doesnt get output to the underlying resource
  def self.boolean(attr)
    define_method(attr) { instance_variable_get :"@#{attr}" }

    type = ActiveRecord::Type::Boolean.new

    define_method(:"#{attr}=") do |v|
      instance_variable_set :"@#{attr}", type.cast(v)
    end

    alias_method :"#{attr}?", attr
  end

  # @TODO This is used just to do multiple booleans !!  (crazy)
  def self.booleans(*attrs)
    attrs.each(&method(:boolean))
  end

  # @TODO Decide whether to change or not
  def self.i18n_scope
    :activemodel
  end

  # @TODO This is for compatibility with old code and is naughty as it is effectively
  # bypassing strong parameters
  # @TODO Work out how to remove this

  # This is to force everything in to thinking we are doing an update all the time ( needed for I18n labels etc.. )
  # @TODO Review if this is really required
  def persisted?
    true
  end

  # Bypasses original save and saves the target
  # @TODO Consider separating persistence from form objects
  # @TODO Why is this calling stuff on target and resource ?
  def save
    return false unless valid?

    run_callbacks :save do
      ActiveRecord::Base.transaction do
        mapped_attributes = __custom_mappings.keys.map(&:to_s)
        target.update attributes.except(*(transient_attributes.map(&:to_s) + mapped_attributes)) unless target.frozen?
        custom_mappings(__custom_mappings)
        resource.save
      end
    end
  end

  # Resource methods

  # Loads the form object with values from the target
  def reload
    return if target.nil?

    (attributes.keys - __custom_mappings.keys.map(&:to_s)).each do |key|
      send "#{key}=", target.try(key)
    end
    custom_mappings(__custom_mappings)
  end

  private

  attr_writer :resource

  def custom_mappings(mappings)
    mappings.each_pair do |attr, options|
      object_to_read_from = options[:to]
      raise "Unknown mapping 'to' method #{object_to_read_from}" unless respond_to?(object_to_read_from)

      send "#{attr}=", send(object_to_read_from).send(attr)
    end
  end

  def clean_strings
    @attributes.each_value do |attr|
      next unless attr.type.is_a?(ActiveModel::Type::String) && attr.value.is_a?(String)

      if attr.value.frozen?
        attr.value = attr.value.strip!
      else
        attr.value.strip!
      end
    end
  end

end