Extension for ActiveRecord to make any changes for an objects attributes protected and create a seperate ChangeRequest which needs to be approved
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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