# ActsAsStatused # This is kind of like a state machine, but the statuses only go forward. # # Initialize with a set of statuses like [:submitted, :approved, :declined]. Creates the following: # scope :approved # approved?, was_approved?, approved_at, approved_by, approved!, unapproved! module ActsAsStatused extend ActiveSupport::Concern module Base # acts_as_statused :pending, :approved, :declined, option_key: :option_value def acts_as_statused(*args) options = args.extract_options! statuses = Array(args).compact if statuses.blank? || statuses.any? { |status| !status.kind_of?(Symbol) } raise 'acts_as_statused expected one or more statuses' end @acts_as_statused_options = options.merge(statuses: statuses) include ::ActsAsStatused end end included do acts_as_statused_options = @acts_as_statused_options attr_accessor :current_user effective_resource do status :string, permitted: false status_steps :text, permitted: false end serialize :status_steps, Hash const_set(:STATUSES, acts_as_statused_options[:statuses]) before_validation do self.status ||= self.class.const_get(:STATUSES).first # Set an existing belongs_to automatically if respond_to?("#{status}_by") && send("#{status}_by").blank? self.send("#{status}_by=", current_user) end # Set an existing timestamp automatically if respond_to?("#{status}_at") && send("#{status}_at").blank? self.send("#{status}_at=", Time.zone.now) end if current_user.present? self.status_steps["#{status}_by_id".to_sym] ||= current_user.id self.status_steps["#{status}_by_type".to_sym] ||= current_user.class.name end self.status_steps["#{status}_at".to_sym] ||= Time.zone.now end validates :status, presence: true, inclusion: { in: const_get(:STATUSES).map(&:to_s) } # Create an received scope and approved? method for each status acts_as_statused_options[:statuses].each do |sym| sym_at = "#{sym}_at".to_sym sym_by = "#{sym}_by".to_sym sym_by_id = "#{sym}_by_id".to_sym sym_by_type = "#{sym}_by_type".to_sym scope(sym, -> { where(status: sym.to_s) }) # approved? define_method("#{sym}?") { status == sym.to_s } # was_approved? define_method("was_#{sym}?") { send(sym_at).present? } # approved_at define_method(sym_at) { self[sym_at.to_s] || status_steps[sym_at] } # approved_by_id define_method(sym_by_id) { self[sym_by_id.to_s] || status_steps[sym_by_id] } # approved_by_type define_method(sym_by_type) { self[sym_by_type.to_s] || status_steps[sym_by_type] } # approved_by define_method(sym_by) do user = (super() if attributes.key?(sym_by_id.to_s)) user ||= begin id = status_steps[sym_by_id] klass = status_steps[sym_by_type] klass.constantize.find(id) if id.present? && klass.present? end end # approved_at= define_method("#{sym_at}=") do |value| super(value) if attributes.key?(sym_at.to_s) status_steps[sym_at] = value end # approved_by_id= define_method("#{sym_by_id}=") do |value| super(value) if attributes.key?(sym_by_id.to_s) status_steps[sym_by_id] = value end # approved_by_type= define_method("#{sym_by_type}=") do |value| super(value) if attributes.key?(sym_by_type.to_s) status_steps[sym_by_type] = value end # approved_by= define_method("#{sym_by}=") do |value| super(value) if attributes.key?(sym_by_id.to_s) status_steps[sym_by_id] = value&.id status_steps[sym_by_type] = value&.class&.name end # approved! define_method("#{sym}!") do |atts = {}| raise 'expected a Hash of passed attributes' unless atts.kind_of?(Hash) update!(atts.merge(status: sym)) end # unapproved! define_method("un#{sym}!") do self.status = nil if (status == sym.to_s) if respond_to?("#{sym}_at") && send("#{sym}_at").present? self.send("#{sym}_at=", nil) end if respond_to?("#{sym}_by") && send("#{sym}_by").present? self.send("#{sym}_by=", nil) end status_steps.delete(sym_at) status_steps.delete(sym_by_id) status_steps.delete(sym_by_type) true end end end module ClassMethods def acts_as_statused?; true; end end end