Skip to content

Instantly share code, notes, and snippets.

@krtschmr
Last active October 4, 2019 08:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save krtschmr/71ba725a723149ca515e638a7a2a3d59 to your computer and use it in GitHub Desktop.
Save krtschmr/71ba725a723149ca515e638a7a2a3d59 to your computer and use it in GitHub Desktop.
Extension for ActiveRecord to make any changes for an objects attributes protected and create a seperate ChangeRequest which needs to be approved
# usage
# your model
# need_admin_approval fields: [:name, :description], if: :published?, unless: :trusted_user?
module NeedAdminApproval
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
attr_accessor :admin_approval_fields, :admin_approval_if, :admin_approval_unless
def need_admin_approval(options = {})
return if included_modules.include?(NeedAdminApproval::ContentMethods)
send :include, NeedAdminApproval::ContentMethods
self.admin_approval_fields = options.fetch(:fields, nil)
self.admin_approval_if = options.fetch(:if, true)
self.admin_approval_unless = options.fetch(:unless, false)
raise("fields needs to be specifield to use admin_approvals") unless admin_approval_fields
has_many :pending_change_requests, ->(obj){ where(object_type: obj.class.to_s, state: :pending) }, foreign_key: :object_id, class_name: "ChangeRequest"
has_many :change_requests, ->(obj){ where(object_type: obj.class.to_s) }, foreign_key: :object_id, class_name: "ChangeRequest"
end
end
module ContentMethods
def self.included(base)
base.extend ClassMethods
end
def admin_approval_fields
self.class.admin_approval_fields
end
def admin_approval_if
self.class.admin_approval_if
end
def admin_approval_unless
self.class.admin_approval_unless
end
def update(attributes={})
# strong parameters isn't working as a hash anymore
# with rails 5.1
attrs = attributes.dup.to_h.symbolize_keys
if approval_neccessary?(attrs)
attributes = attrs
# we bypass the validations so we have to make sure the attributes are safe
# we assing all attributes and then call valid?
assign_attributes(attributes)
if valid?
attributes_for_approval = {}
admin_approval_fields.each do |field|
# if the field exists, take it from attributes hash and
# store its value for approval
if pending_changes_for?(field)
raise ChangeRequest::AlreadyPendingRequest, "#{field} has already a pending request"
end
attributes_for_approval[field] = attributes.delete(field) if attributes[field]
end
# those attributes left are those who don't need an approval
# merge the timestamp, if any attributes are left
attributes_without_approval = attributes
attributes_without_approval.merge!(updated_at: Time.current) if attributes_without_approval.any? && self.class.has_attribute?(:updated_at)
begin
change_request = nil
self.transaction do
change_request = ChangeRequest.create! object: self, fields: attributes_for_approval
raise unless change_request.persisted?
self.update_columns(attributes_without_approval) if attributes_without_approval.any?
self.reload
end
return change_request
rescue => e
raise e
end
else
# not valid
return self
end
end
super
end
def pending_changes?
change_requests.pending.count > 0
end
def pending_changes_for?(field)
pending_change_requests.any? {|x| x.fields[field].present? }
end
private
def approval_neccessary?(attributes)
admin_approval_fields && approval_callbacks? && wants_update_fields_with_approval?(attributes)
end
def approval_callbacks?
approve_if && !approve_unless
end
def approve_if
return true if admin_approval_if === true
if admin_approval_if.is_a?(Symbol)
self.send(admin_approval_if)
else # asume its proc
self.instance_eval(&admin_approval_if)
end
end
def approve_unless
return false if admin_approval_unless === false
if admin_approval_unless.is_a?(Symbol)
self.send(admin_approval_unless)
else # asume its proc
self.instance_eval(&admin_approval_unless)
end
end
def wants_update_fields_with_approval?(attributes)
attributes.any?{|k,_v| admin_approval_fields.include?(k)}
end
end
end
ApplicationRecord.class_eval do
include NeedAdminApproval
end
class ChangeRequest < ApplicationRecord
class AlreadyPendingRequest < Exception; end
belongs_to_object #it needs to have a relation to some object
serialize :fields, Hash
state_machine initial: :pending do
state :approved
state :declined
event :approve do transition :pending => :approved end
event :decline do transition :pending => :declined end
end
def approve!
self.transaction do
commit_changes!
super
end
end
def commit_changes!
raise unless pending?
committed_changes = fields.dup
committed_changes.merge!(updated_at: Time.current) if object.class.has_attribute? :updated_at
object.update_columns(committed_changes)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment