module DattsRight module InstanceMethods def add_dynamic_attribute(name, object_type, value=nil) key = name.to_s.underscore return false if self.class.columns_hash[key] # if key already exists as a normal attribute unless dynamic_attribute?(key) new_dynamic_attribute = dynamic_attributes.new :attr_key => key, :object_type => object_type, "#{object_type}_value".to_sym => value @dynamic_attributes_cache[key.to_sym] = new_dynamic_attribute return new_dynamic_attribute end return false end def add_dynamic_attribute!(name, object_type, value=nil) dynamic_attribute = add_dynamic_attribute(name, object_type, value) dynamic_attribute.save if dynamic_attribute key = name.to_s.underscore end def remove_dynamic_attribute(name) # Remove from the cache @dynamic_attributes_cache.delete(name.to_sym) # Then remove from the db dynamic_attribute = dynamic_attributes.find_by_attr_key(name.to_s) dynamic_attribute.destroy if dynamic_attribute end # Give users access to the cache def dynamic_attribute_details(key) @dynamic_attributes_cache[key] end # Determines if the given attribute is a dynamic attribute. def dynamic_attribute?(attr) !@dynamic_attributes_cache[attr.to_sym].nil? end def update_dynamic_attributes(attributes) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do attributes.symbolize_keys.each do |k, v| self.write_dynamic_attribute(k, v) end save end end def update_dynamic_attributes!(attributes) with_transaction_returning_status do attributes.symbolize_keys.each do |k, v| self.write_dynamic_attribute(k, v) end save! end end # Like AR::Base#read_attribute def read_dynamic_attribute(attr_name) attr_name = attr_name.to_sym if dynamic_attribute?(attr_name) #puts "Reading #{attr_name}. The whole cache: #{@dynamic_attributes_cache.inspect}" @dynamic_attributes_cache[attr_name].value end end # Like AR::Base#write_attribute def write_dynamic_attribute(attr_name, value) #puts "attempting to write: #{attr_name} = #{value}" attr_name = attr_name.to_sym if dynamic_attribute?(attr_name) #puts "#{attr_name} is a dynamic_attribute" #puts "Writing @dynamic_attributes_cache[:#{attr_name}].value = #{value}" dynamic_attribute = @dynamic_attributes_cache[attr_name] dynamic_attribute.value = value #puts "In write_dynamic_attribute. Full cache: #{@dynamic_attributes_cache.inspect}" return dynamic_attribute.value end end def attributes=(new_attributes, guard_protected_attributes = true) return unless new_attributes.is_a?(Hash) attributes = new_attributes.stringify_keys multi_parameter_attributes = [] attributes = sanitize_for_mass_assignment(attributes) if guard_protected_attributes attributes.each do |k, v| if k.include?("(") multi_parameter_attributes << [ k, v ] else #respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}") begin #puts "Attempt to set super #{k} to #{v}" #puts "Checking to see if #{self.inspect} responds to #{k}= ........... #{self.class.name}##{self.class.respond_to?(:"#{k}=")}, or the record itself: #{respond_to?(:"#{k}=")}" respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(ActiveRecord::UnknownAttributeError, "unknown attribute: #{k}") #send("#{k}=", v) #puts "Set super #{k} to #{v}" rescue ActiveRecord::UnknownAttributeError => e #puts "ActiveRecord::UnknownAttributeError was raised: #{e}, so we now check to see if '#{k}' is dynamic_attribute" if dynamic_attribute?(k) write_dynamic_attribute("#{k}", v) else raise ActiveRecord::UnknownAttributeError, "unknown attribute: #{k}" end end end end assign_multiparameter_attributes(multi_parameter_attributes) end def defining_record return nil if dynamic_attributes_options[:of].nil? send dynamic_attributes_options[:of].to_s end def create_dynamic_attribute_definition_if_needed if dynamic_attributes_options[:definition] DynamicAttributeDefinition.create :attribute_defineable_id => self.id, :attribute_defineable_type => self.class.name, :definition => {} end end def add_definition(key, value) if key key = key.to_sym #puts "add_definition(:#{key}, #{value.inspect}). current definition: #{definition.nil?}" if dynamic_attributes_options[:definition] #puts ":definition is true, so let's see if definition[:#{key}] already exists" # Checking definition straight seems to cause a problem. Is this a bug with Rails? # The problem was like this (assuming definition was actually nil): # definition.nil? # false # definition.inspect # nil # definition.class.name # NilClass # # How in the world can .nil? return false when it was nil?? dynamic_attribute_definition.definition ||= {} if definition[key] raise AlreadyDefined, "#{key} is already defined" else definition.merge!({key => value}) end else raise NoDefinitionError end else raise AttributeKeyRequired end end def add_definitions(*args) attributes = args attributes.compact! if attributes#remove the nil items #puts "args after compacting: #{attributes.inspect}" attributes.flatten! if args.first.is_a?(Array) && attributes #puts "args after flattening: #{attributes.inspect}" attributes.each do |item| #puts "Working on #{item.inspect} is is a hash? (#{item.is_a?(Hash)}) or something else?" item.each do |k, v| #puts "Working on this k,v pair: #{k.inspect} => #{v.inspect}" if v.is_a?(Hash) # item is like :robot => {:object_type => "text"}, :robot@ => {:object_type => "text"} #puts "#{v} IS a hash" add_definition k, v else # v is not a hash; item is like {"name"=>"A key", "attr_key"=>"a_key"}, {"name"=>"B key", "attr_key"=>"b_key"} # Sometimes the item is a ActiveRecord::HashWithIndifferentAccess, which doesn't have the method symbolize_keys!, so we do it manually #item = item.symbolize_keys # {:name=>"A key", :description=>"asd", :attr_key=>"a_key"} #puts "item is symbolized: #{item.inspect}" attr_key = item.delete("attr_key") #puts "This is the attr_key: #{attr_key}" if attr_key # we only want to work on it if there's an attr_key attr_key = attr_key.to_sym #puts "Adding: :#{attr_key}, #{item.inspect}" add_definition(attr_key, item) end end end end end def update_definition(key, new_values={}) if dynamic_attributes_options[:definition] if definition && definition[key] attr_key = new_values.symbolize_keys[:attr_key] new_values.each do |k, v| definition[key][k] = v unless k.to_s == "attr_key" end #puts "attr_key is #{attr_key}" if attr_key #puts "Adding definition: #{attr_key} => #{definition[key]}" add_definition(attr_key, definition[key]) #puts "Removing definition: #{key} => #{definition[key]}" remove_definition(key) end else raise NotDefinedError, "#{key} is not defined" end else raise NoDefinitionError end end def update_definitions(hash={}) hash.each do |k, v| update_definition k, v end end def remove_definition(key) if key key = key.to_sym if dynamic_attributes_options[:definition] if definition && definition[key] definition.delete(key) else raise NotDefinedError, "#{key} is not defined" end else raise NoDefinitionError end end end def remove_definitions(*array) if array array.each do |a| remove_definition a end end end private # Called after validation on update so that dynamic attributes behave # like normal attributes in the fact that the database is not touched # until save is called. def build_dynamic_attributes @dynamic_attributes_cache ||= {} @dynamic_attributes_cache.each { |k, v| v.save } end def cache_dynamic_attributes @dynamic_attributes_cache = {} dynamic_attributes.each do |dynamic_attribute| #puts "Caching: #{dynamic_attribute.inspect}" @dynamic_attributes_cache[dynamic_attribute.attr_key.to_sym] = dynamic_attribute end @dynamic_attributes_cache end def method_missing(method_id, *args, &body) begin super(method_id, *args, &body) rescue NoMethodError => e attr_name = method_id.to_s.sub(/\=$/, '') if dynamic_attribute?(attr_name) method_id.to_s =~ /\=$/ ? write_dynamic_attribute(attr_name, args[0]) : read_dynamic_attribute(attr_name) else raise NoMethodError end end end def save_dynamic_attribute_definition dynamic_attribute_definition.save if dynamic_attribute_definition end def inherit_definition #puts "In inherit_definition" #puts "------- #{dynamic_attributes_options[:of]}" #dynamic_attribute_definition.create if dynamic_attributes_options[:definition] if dynamic_attributes_options[:of] #puts "There is a defining record of #{self.class.name}##{self.name} in #{defining_record.inspect}. #{defining_record.dynamic_attribute_definition.inspect}" if defining_record defining_record.definition.each do |k, v| datt = add_dynamic_attribute(k, v[:object_type]) datt.dynamic_attribute_definition #puts "Added #{datt.inspect}" end end end end alias :add_datt :add_dynamic_attribute alias :add_datt! :add_dynamic_attribute! alias :remove_datt :remove_dynamic_attribute alias :read_datt :read_dynamic_attribute alias :write_datt :write_dynamic_attribute alias :datt_details :dynamic_attribute_details alias :update_dynamic_attribute :write_dynamic_attribute end end