module ActiveMetadata CONFIG = File.exists?('config/active_metadata.yml') ? YAML.load_file('config/active_metadata.yml')[Rails.env] : {} CONFIG['cache_expires_in'] ||= 60 ## Define ModelMethods module Base require 'paperclip' require 'active_metadata/persistence/persistence' require 'active_metadata/helpers' def self.included(klass) klass.class_eval do extend Config end end module Config def acts_as_metadata *args before_update :manage_concurrency after_save :save_history attr_accessor :conflicts attr_accessor :active_metadata_timestamp class_variable_set("@@metadata_id_from", args.empty? ? nil : args[0][:metadata_id_from]) include ActiveMetadata::Base::InstanceMethods include ActiveMetadata::Persistence end end module InstanceMethods include ActiveMetadata::Helpers def self.included(klass) [:notes, :attachments, :history].each do |item| klass.send(:define_method, "#{item.to_s}_cache_key".to_sym) do |field| "#{Rails.env}/active_metadata/#{item.to_s}/#{self.class}/#{metadata_id}/#{field}/" end end [:fatals, :warnings].each do |conf| klass.send(:define_method, "has_#{conf.to_s}_conflicts?".to_sym) do return false if self.conflicts.nil? || self.conflicts[conf.to_sym].nil? || self.conflicts[conf.to_sym].empty? true end end end def metadata_id metadata_root.id end # Normalize the active_metadata_timestamp into a float to be comparable with the history def am_timestamp ts = metadata_root.active_metadata_timestamp return nil if ts.nil? ts = ts.to_f end def current_user_id if User.respond_to?(:current) && !User.current.nil? User.current.id else nil end end def metadata_root metadata_id_from = self.class.class_variable_get("@@metadata_id_from") return self if metadata_id_from.nil? receiver = self metadata_id_from.each do |item| receiver = receiver.send item end receiver end # Resolve concurrency using the provided timestamps and the active_metadata histories. # Conflicts are stored into @conflicts instance variable # Timestamp used for versioning can be passed both as : # ==== # * @object.active_metadata_timestamp = .... # * @object.update_attributes :active_metadata_timestamp => ... # # Check is skipped if no timestamp exists. A check is made also for parents defined via acts_as_metadata method # # Conflicts # ==== # * @conflicts[:warnings] contains any conflict that "apply cleanly" or where the submit value has not been modified by the user that is saving # the data # # * @conflicts[:fatals] contains any conflict that "do not apply cleanly" or where the passed value does not match the last history value and was modified # by the user that is submittig the data # def manage_concurrency timestamp = self.am_timestamp return if timestamp.nil? #if no timestamp no way to check concurrency so just skip self.conflicts = { :warnings => [], :fatals => [] } # scan params self.attributes.each do |key, val| # ensure the query order histories = history_for key.to_sym, "created_at DESC" # if history does not exists yet cannot be a conflict OR # if value has no change respect to the latest db saved we have no conflict next if histories.count == 0 || self.changes[key].nil? latest_history = histories.first #if the timestamp is previous of the last history change if timestamp < latest_history.created_at.to_f # there is a conflict so ensure the actual value self[key.to_sym] = self.changes[key][0] # We have a conflict. # Check if the actual submission has been modified histories.each do |h| # Looking for the value that was loaded by the user. First history with a ts that is younger than the form ts next if timestamp > h.created_at.to_f # History stores values as strings so any boolean is stored as "0" or "1" # We need to translate the params passed for a safer comparison. if self.column_for_attribute(key).type == :boolean b_val = to_bool(h.value) end if [h.value, b_val].include? val self.conflicts[:warnings] << {key.to_sym => [val, h]} else self.conflicts[:fatals] << {key.to_sym => [val, h]} end end end end end end # InstanceMethods end end ::ActiveRecord::Base.send :include, ActiveMetadata::Base