Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'release/v0.0.33'

  • Loading branch information...
commit 976113cf35bd1bb916eee6e3d67d6d9763c944fc 2 parents da575b7 + 969c0cc
@ramontayag authored
View
2  VERSION
@@ -1 +1 @@
-0.0.32
+0.0.33
View
22 lib/datts_right.rb
@@ -1,7 +1,21 @@
require 'datts_right/base'
-require 'datts_right/instance_methods'
-require 'datts_right/dynamic_attribute'
-require 'datts_right/dynamic_attribute_definition'
+require 'datts_right/dynamic_attribute_methods'
+require 'datts_right/definition_methods'
+require 'datts_right/models/dynamic_attribute'
+require 'datts_right/models/dynamic_attribute_definition'
require 'datts_right/errors'
-ActiveRecord::Base.extend DattsRight::Base
+module DattsRight
+ def has_dynamic_attributes(options={})
+ cattr_accessor :dynamic_attributes_options
+ self.dynamic_attributes_options = options
+ include DattsRight::Base
+ include DattsRight::DynamicAttributeMethods
+ include DattsRight::DefinitionMethods
+ end
+
+ alias :has_datts :has_dynamic_attributes
+end
+
+# Apply DattsRight module to ActiveRecord, so that has_dynamic_attributes or has_datts will be available to ActiveRecord
+ActiveRecord::Base.extend DattsRight
View
116 lib/datts_right/base.rb
@@ -1,114 +1,9 @@
module DattsRight
module Base
- def has_dynamic_attributes(options={})
- include DattsRight::InstanceMethods
- cattr_accessor :dynamic_attributes_options
- self.dynamic_attributes_options = options
-
- has_many :dynamic_attributes, :as => :attributable, :dependent => :destroy, :order => :id
- has_one :dynamic_attribute_definition, :as => :attribute_defineable, :dependent => :destroy
- after_save :save_dynamic_attribute_definition
- after_create :create_dynamic_attribute_definition_if_needed, :inherit_definition!
- delegate :definition, :definition=, :to => :dynamic_attribute_definition
-
- # Carry out delayed actions before save
- before_save :build_dynamic_attributes
-
- after_find :cache_dynamic_attributes
- default_scope includes(:dynamic_attributes).includes(:dynamic_attribute_definition)
-
- #validate :must_be_reflected_by_definer
-
- # scope :scope_self when looking through attributes so we don't look through all dynamic_attributes
- # Why? What if you have Friend and Page models.
- # * Some Phone records have a dynamic_attribute :price
- # * Some Page records have a dynamic_attribute :price
- #
- # When we do Page.find_by_price(400) we want to search only the dynamic_attributes that belong to Page
- # and we want to disregard the rest of the dynamic_attributes.
- scope :scope_self, lambda { joins(:dynamic_attributes).where("dynamic_attributes.attributable_type = :klass", :klass => self.name) }
- scope :with_datt_key, lambda { |args| with_dynamic_attribute_key(args) }
- scope :with_dynamic_attribute_key, lambda { |datt_key| scope_self.joins(:dynamic_attributes).where("dynamic_attributes.attr_key = :datt_key", :datt_key => datt_key)}
- scope :with_datt_type, lambda { |args| with_dynamic_attribute_type(args) }
- scope :with_dynamic_attribute_type, lambda { |object_type| scope_self.joins(:dynamic_attributes).where("object_type = :object_type", :object_type => object_type) }
-
- scope :order_by_datt, lambda { |attr_key_with_order, object_type| order_by_dynamic_attribute(attr_key_with_order, object_type) }
- scope :order_by_dynamic_attribute, lambda { |attr_key_with_order, object_type|
- # possible attr_key_with_order forms: "field_name", "field_name ASC", "field_name DESC"
- split_attr_key_with_order = attr_key_with_order.split(" ")
- attr_key = split_attr_key_with_order.first
- order_by = split_attr_key_with_order.last if split_attr_key_with_order.size > 1
- order_value = "dynamic_attributes.#{object_type}_value"
- order_value << " #{order_by}" if order_by
- scope_self.with_dynamic_attribute_key(attr_key).joins(:dynamic_attributes).with_dynamic_attribute_type(object_type).order(order_value)
- }
-
- scope :where_datt, lambda { |opts| where_dynamic_attribute(opts) }
- scope :where_datts, lambda { |opts| where_dynamic_attribute(opts) }
- scope :where_dynamic_attributes, lambda { |opts| where_dynamic_attribute(opts) }
- scope :where_dynamic_attribute, lambda { |opts|
- # TODO accept stuff other than the normal hash
- # Lifted from AR::Relation#build_where
- attributes = case opts
- when String, Array
- self.expand_hash_conditions_for_aggregates(opts)
- when Hash
- opts
- end
- results = self
- attributes.each do |k, v|
- conditions = "exists (" +
- "select 1 from dynamic_attributes dynamic_attribute where " +
- "#{self.table_name}.id = dynamic_attribute.attributable_id " +
- "and dynamic_attribute.attributable_type = :attributable_type " +
- "and dynamic_attribute.attr_key = :attr_key and dynamic_attribute.#{DynamicAttribute.attr_column(v)} = :value" +
- ")"
- results = results.where(conditions, :attributable_type => self.name, :attr_key => k.to_s, :value => v)
- end
- results
- }
-
- # Used when you have already existing records that have don't have definitions, and you want them to
- def self.create_definitions!
- self.all.each do |record|
- record.create_dynamic_attribute_definition_if_needed
- end
- end
-
- def self.defines?(klass=nil)
- return false unless !dynamic_attributes_options[:defines].nil? && !dynamic_attributes_options[:defines].empty?
- #puts "There is [:defines], and it's not empty"
- if klass
- klass = klass.class unless klass.is_a?(Class)
- klass_symbol = klass.name.downcase.to_sym
- dynamic_attributes_options[:defines].include?(klass_symbol)
- else
- true
- end
- end
-
- def self.defines
- dynamic_attributes_options[:defines] if defines?
- end
-
- def self.defined?
- dynamic_attributes_options[:of]
- end
-
- def self.defined_by?(arg)
- return false unless self.defined?
- symbol = if arg.is_a?(Class)
- arg.name.underscore.to_sym
- elsif arg.is_a?(Symbol)
- arg
- else # should be an instance
- arg.class.name.underscore.to_sym
- end
- dynamic_attributes_options[:of] == symbol
- end
+ extend ActiveSupport::Concern
+ included do
private
-
def self.method_missing(method_id, *arguments)
# TODO better way to hook this into the rails code, and not define my own
begin # Prioritize ActiveRecord's method_missing
@@ -140,7 +35,7 @@ def self.method_missing(method_id, *arguments)
# Override AR::Base#respond_to? so we can return the matchers even if the
# attribute doesn't exist in the actual columns. Is this expensive?
- def respond_to?(method_id, include_private=false)
+ def self.respond_to?(method_id, include_private=false)
# TODO perhaps we could save a cache somewhere of all methods used by
# any of the records of this class. that way, we can make this method
# act a bit more like AR::Base#respond_to?
@@ -154,8 +49,9 @@ def respond_to?(method_id, include_private=false)
super
end
- end
+ end # EO included
- alias :has_datts :has_dynamic_attributes
+ module InstanceMethods
+ end # EO InstanceMethods
end
end
View
223 lib/datts_right/definition_methods.rb
@@ -0,0 +1,223 @@
+module DattsRight
+ module DefinitionMethods
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :dynamic_attribute_definition, :as => :attribute_defineable, :dependent => :destroy
+ after_create :create_dynamic_attribute_definition_if_needed, :inherit_definition!
+ after_save :save_dynamic_attribute_definition
+ delegate :definition, :definition=, :to => :dynamic_attribute_definition
+
+ # Used when you have already existing records that have don't have definitions, and you want them to
+ def self.create_definitions!
+ self.all.each do |record|
+ record.create_dynamic_attribute_definition_if_needed
+ end
+ end
+
+ def self.defines?(klass=nil)
+ return false unless !dynamic_attributes_options[:defines].nil? && !dynamic_attributes_options[:defines].empty?
+ if klass
+ klass = klass.class unless klass.is_a?(Class)
+ klass_symbol = klass.name.downcase.to_sym
+ dynamic_attributes_options[:defines].include?(klass_symbol)
+ else
+ true
+ end
+ end
+
+ def self.defines
+ dynamic_attributes_options[:defines] if defines?
+ end
+
+ def self.defined?
+ dynamic_attributes_options[:of]
+ end
+
+ def self.defined_by?(arg)
+ return false unless self.defined?
+ symbol = if arg.is_a?(Class)
+ arg.name.underscore.to_sym
+ elsif arg.is_a?(Symbol)
+ arg
+ else # should be an instance
+ arg.class.name.underscore.to_sym
+ end
+ dynamic_attributes_options[:of] == symbol
+ end
+
+ end
+
+ module InstanceMethods
+ 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 self.defines?
+ DynamicAttributeDefinition.create :attribute_defineable_id => self.id, :attribute_defineable_type => self.class.name, :definition => {}
+ end
+ end
+
+ # 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??
+ def add_definition(key, value)
+ raise AttributeKeyRequired unless key
+ key = key.to_sym
+ raise NoDefinitionError if dynamic_attributes_options[:defines].nil? || dynamic_attributes_options[:defines].empty?
+ dynamic_attribute_definition.definition ||= {}
+ raise(AlreadyDefinedError, "#{key} is already defined") if definition[key]
+ definition.merge!({key => value})
+ 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={})
+ raise NoDefinitionError unless self.defines?
+ raise NotDefinedError, "#{key} is not defined" unless 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}, key is #{key}"
+ if attr_key && attr_key != key.to_s
+ #puts "Adding definition: #{attr_key} => #{definition[key]}"
+ add_definition(attr_key, definition[key])
+ #puts "Removing definition: #{key} => #{definition[key]}"
+ remove_definition(key)
+ 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
+ raise NoDefinitionError unless self.defines?
+ raise NotDefinedError, "#{key} is not defined" unless definition && definition[key]
+ definition.delete(key)
+ end
+ end
+
+ def remove_definitions(*array)
+ if array
+ array.each do |a|
+ remove_definition a
+ end
+ end
+ end
+
+ # Adds dynamic attributes to inheriting model, based on the definition of the defining model.
+ # class InheritingModel < AR::Base
+ # has_dynamic_attributes :of => :defining_model
+ # end
+ #
+ # The DefiningModel should have the code:
+ # class DefiningModel < AR::Base
+ # has_dynamic_attributes :defines => [:inheriting_model]
+ # end
+ #
+ # Example:
+ # @defining_model.add_definitions(:name => {:object_type => "string"}, :body => {:object_type => "text"})
+ # @defining_model.save
+ # InheritingModel.create # creates an instance with dynamic attributes: name and body, that are "string" and "text", respectively
+ #
+ # Calling this method manually only _adds_ to the inheriting instance you call it on. If you remove some definitions from the defining model, then you call @inheriting_model.inherit_definition!@, the new definitions will be added. If there are no new definitions, nothing will happen.
+ def inherit_definition
+ #puts "In inherit_definition for #{self.class.name}##{self.id} -- defined? #{self.defined?}; defining_record: #{self.defining_record.inspect}"
+ #puts "------- #{dynamic_attributes_options[:of]}"
+ #dynamic_attribute_definition.create if dynamic_attributes_options[:definition]
+ if self.defined? && defining_record
+ #puts "There is a defining record, this is the definition: #{defining_record.definition.inspect}"
+ defining_record.definition.each do |k, v|
+ datt = self.add_dynamic_attribute(k, v[:object_type])
+ #datt.dynamic_attribute_definition
+ #puts "Added #{datt.inspect}"
+ end
+ end
+ end
+
+ def inherit_definition!
+ inherit_definition
+ save
+ end
+
+ # Adds and removes dynamic attributes based on the defining record
+ def sync_definition!
+ defining_record.definition.each do |k, v|
+ add_dynamic_attribute(k, v[:object_type]) if dynamic_attribute_details(k).nil?
+ end
+ @dynamic_attributes_cache.each do |k, v|
+ #puts "Remove #{k}?"
+ remove_dynamic_attribute(k) if v.definer.nil?
+ end
+ self.save
+ end
+
+ # Returns true if this defines any other class
+ def defines?(klass=nil)
+ self.class.defines?(klass)
+ end
+
+ # Returns true if this is defined by any other class
+ def defined?
+ self.class.defined?
+ end
+
+ # Returns true if the calling instance is defined by the argument
+ def defined_by?(arg)
+ self.class.defined_by?(arg)
+ end
+
+ # Returns an array of symbols of the classes that this defines
+ def defines
+ self.class.defines
+ end
+
+ private
+
+ def save_dynamic_attribute_definition
+ dynamic_attribute_definition.save if dynamic_attribute_definition
+ end
+ end
+ end
+end
View
206 lib/datts_right/dynamic_attribute_methods.rb
@@ -0,0 +1,206 @@
+module DattsRight
+ module DynamicAttributeMethods
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :dynamic_attributes, :as => :attributable, :dependent => :destroy, :order => :id
+ before_save :build_dynamic_attributes
+ after_find :cache_dynamic_attributes
+ default_scope includes(:dynamic_attributes).includes(:dynamic_attribute_definition)
+
+ # scope :scope_self when looking through attributes so we don't look through all dynamic_attributes
+ # Why? What if you have Friend and Page models.
+ # * Some Phone records have a dynamic_attribute :price
+ # * Some Page records have a dynamic_attribute :price
+ #
+ # When we do Page.find_by_price(400) we want to search only the dynamic_attributes that belong to Page
+ # and we want to disregard the rest of the dynamic_attributes.
+ scope :scope_self, lambda { joins(:dynamic_attributes).where("dynamic_attributes.attributable_type = :klass", :klass => self.name) }
+ scope :with_datt_key, lambda { |args| with_dynamic_attribute_key(args) }
+ scope :with_dynamic_attribute_key, lambda { |datt_key| scope_self.joins(:dynamic_attributes).where("dynamic_attributes.attr_key = :datt_key", :datt_key => datt_key)}
+ scope :with_datt_type, lambda { |args| with_dynamic_attribute_type(args) }
+ scope :with_dynamic_attribute_type, lambda { |object_type| scope_self.joins(:dynamic_attributes).where("object_type = :object_type", :object_type => object_type) }
+
+ scope :order_by_datt, lambda { |attr_key_with_order, object_type| order_by_dynamic_attribute(attr_key_with_order, object_type) }
+ scope :order_by_dynamic_attribute, lambda { |attr_key_with_order, object_type|
+ # possible attr_key_with_order forms: "field_name", "field_name ASC", "field_name DESC"
+ split_attr_key_with_order = attr_key_with_order.split(" ")
+ attr_key = split_attr_key_with_order.first
+ order_by = split_attr_key_with_order.last if split_attr_key_with_order.size > 1
+ order_value = "dynamic_attributes.#{object_type}_value"
+ order_value << " #{order_by}" if order_by
+ scope_self.with_dynamic_attribute_key(attr_key).joins(:dynamic_attributes).with_dynamic_attribute_type(object_type).order(order_value)
+ }
+
+ scope :where_datt, lambda { |opts| where_dynamic_attribute(opts) }
+ scope :where_datts, lambda { |opts| where_dynamic_attribute(opts) }
+ scope :where_dynamic_attributes, lambda { |opts| where_dynamic_attribute(opts) }
+ scope :where_dynamic_attribute, lambda { |opts|
+ # TODO accept stuff other than the normal hash
+ # Lifted from AR::Relation#build_where
+ attributes = case opts
+ when String, Array
+ self.expand_hash_conditions_for_aggregates(opts)
+ when Hash
+ opts
+ end
+ results = self
+ attributes.each do |k, v|
+ conditions = "exists (" +
+ "select 1 from dynamic_attributes dynamic_attribute where " +
+ "#{self.table_name}.id = dynamic_attribute.attributable_id " +
+ "and dynamic_attribute.attributable_type = :attributable_type " +
+ "and dynamic_attribute.attr_key = :attr_key and dynamic_attribute.#{DynamicAttribute.attr_column(v)} = :value" +
+ ")"
+ results = results.where(conditions, :attributable_type => self.name, :attr_key => k.to_s, :value => v)
+ end
+ results
+ }
+ end
+
+ module InstanceMethods
+ def add_dynamic_attribute(name, object_type, value=nil)
+ key = name.to_s.underscore
+ raise(DattsRight::AlreadyDefinedError, "#{name} is an existing normal attribute") if self.class.columns_hash[key] # if key already exists as a normal attribute
+ raise(DattsRight::AlreadyDefinedError, "#{name} is an existing dynamic attribute") if self.dynamic_attribute?(key) # if dynamic_attribute already exists in the cache, then we should not add it
+ 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
+ new_dynamic_attribute
+ end
+
+ def add_dynamic_attribute!(name, object_type, value=nil)
+ dynamic_attribute = add_dynamic_attribute(name, object_type, value)
+ dynamic_attribute.save
+ end
+
+ def remove_dynamic_attribute(name)
+ raise(DattsRight::NotDefinedError, "#{name} dynamic attribute does not exist") unless self.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"
+ raise ActiveRecord::UnknownAttributeError, "unknown attribute: #{k}" unless dynamic_attribute?(k)
+ write_dynamic_attribute("#{k}", v)
+ end
+ end
+ end
+
+ assign_multiparameter_attributes(multi_parameter_attributes)
+ 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(/\=$/, '')
+ raise NoMethodError unless dynamic_attribute?(attr_name)
+ method_id.to_s =~ /\=$/ ? write_dynamic_attribute(attr_name, args[0]) : read_dynamic_attribute(attr_name)
+ 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 :update_dynamic_attribute :write_dynamic_attribute
+ alias :datt_details :dynamic_attribute_details
+ end
+ end
+end
View
2  lib/datts_right/errors.rb
@@ -5,7 +5,7 @@ class DattsRightError < StandardError
class NoDefinitionError < DattsRightError
end
- class AlreadyDefined < DattsRightError
+ class AlreadyDefinedError < DattsRightError
end
class NotDefinedError < DattsRightError
View
332 lib/datts_right/instance_methods.rb
@@ -1,332 +0,0 @@
-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 self.defines?
- 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[:defines].nil? || dynamic_attributes_options[:defines].empty?
- raise NoDefinitionError
- else
- #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
- 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={})
- raise NoDefinitionError unless self.defines?
- raise NotDefinedError, "#{key} is not defined" unless 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}, key is #{key}"
- if attr_key && attr_key != key.to_s
- #puts "Adding definition: #{attr_key} => #{definition[key]}"
- add_definition(attr_key, definition[key])
- #puts "Removing definition: #{key} => #{definition[key]}"
- remove_definition(key)
- 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
- raise NoDefinitionError unless self.defines?
- raise NotDefinedError, "#{key} is not defined" unless definition && definition[key]
- definition.delete(key)
- end
- end
-
- def remove_definitions(*array)
- if array
- array.each do |a|
- remove_definition a
- end
- end
- end
-
- # Adds dynamic attributes to inheriting model, based on the definition of the defining model.
- # class InheritingModel < AR::Base
- # has_dynamic_attributes :of => :defining_model
- # end
- #
- # The DefiningModel should have the code:
- # class DefiningModel < AR::Base
- # has_dynamic_attributes :defines => [:inheriting_model]
- # end
- #
- # Example:
- # @defining_model.add_definitions(:name => {:object_type => "string"}, :body => {:object_type => "text"})
- # @defining_model.save
- # InheritingModel.create # creates an instance with dynamic attributes: name and body, that are "string" and "text", respectively
- #
- # Calling this method manually only _adds_ to the inheriting instance you call it on. If you remove some definitions from the defining model, then you call @inheriting_model.inherit_definition!@, the new definitions will be added. If there are no new definitions, nothing will happen.
- def inherit_definition!
- #puts "In inherit_definition"
- #puts "------- #{dynamic_attributes_options[:of]}"
- #dynamic_attribute_definition.create if dynamic_attributes_options[:definition]
- if self.defined? && 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
-
- # Adds and removes dynamic attributes based on the defining record
- def sync_definition!
- defining_record.definition.each do |k, v|
- add_dynamic_attribute(k, v[:object_type]) if dynamic_attribute_details(k).nil?
- end
- @dynamic_attributes_cache.each do |k, v|
- #puts "Remove #{k}?"
- remove_dynamic_attribute(k) if v.definer.nil?
- end
- end
-
- # Returns true if this defines any other class
- def defines?(klass=nil)
- self.class.defines?(klass)
- end
-
- # Returns true if this is defined by any other class
- def defined?
- self.class.defined?
- end
-
- # Returns true if the calling instance is defined by the argument
- def defined_by?(arg)
- self.class.defined_by?(arg)
- end
-
- # Returns an array of symbols of the classes that this defines
- def defines
- self.class.defines
- end
-
- #def must_be_reflected_by_definer
- #errors.add(:base, "#{self.class.name} inherits its definition from #{dynamic_attributes_options[:of]} but that does not define #{self.class.name}.") \
- #unless dynamic_attributes_options[:of].to_s.classify.constantize.defines?(self.class)
- #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
-
- 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
View
0  lib/datts_right/dynamic_attribute.rb → lib/datts_right/models/dynamic_attribute.rb
File renamed without changes
View
0  ...tts_right/dynamic_attribute_definition.rb → ...ht/models/dynamic_attribute_definition.rb
File renamed without changes
View
4 spec/datts_right/add_definition_spec.rb
@@ -1,6 +1,6 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
-describe DattsRight, ".add_definition(key, value)" do
+describe DattsRight, "#add_definition" do
before do
reset_database
end
@@ -18,7 +18,7 @@
c = Category.create
c.definition = {:robot => {:object_type => "string"}}
c.save
- lambda {c.add_definition(:robot, "whatever")}.should raise_error(DattsRight::AlreadyDefined, "robot is already defined")
+ lambda {c.add_definition(:robot, "whatever")}.should raise_error(DattsRight::AlreadyDefinedError, "robot is already defined")
end
it "should raise NoMethodError if it doesn't have definition => true" do
View
18 spec/datts_right/add_dynamic_attribute_spec.rb
@@ -1,6 +1,6 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
-describe DattsRight, ".remove_dynamic_attribute(attr_key)" do
+describe DattsRight, "#add_dynamic_attribute" do
before do
reset_database
@page = Page.create
@@ -16,11 +16,17 @@
lambda { @page.rocks }.should raise_error(NoMethodError)
end
- it "should not explode if the attribute being removed isn't there" do
+ it "should raise DattsRight::NotDefinedError if the attribute being removed isn't there" do
+ lambda { @page.remove_dynamic_attribute(:rocks) }.should raise_error(DattsRight::NotDefinedError, "rocks dynamic attribute does not exist")
+ end
+
+ it "should raise AlreadyDefinedError if adding an attribute that is a normal attribute" do
+ lambda {@page.add_dynamic_attribute(:name, "string")}.should raise_error(DattsRight::AlreadyDefinedError, "name is an existing normal attribute")
+
+ end
+
+ it "should raise AlreadyDefinedError if adding an attribute that is already a dynamic attribute" do
@page.add_dynamic_attribute(:rocks, "string")
- @page.remove_dynamic_attribute(:rocks)
- lambda {
- @page.remove_dynamic_attribute(:rocks)
- }.should_not raise_error(NoMethodError)
+ lambda {@page.add_dynamic_attribute(:rocks, "string")}.should raise_error(DattsRight::AlreadyDefinedError, "rocks is an existing dynamic attribute")
end
end
View
50 spec/datts_right/add_dynamic_attributes_spec.rb
@@ -1,50 +0,0 @@
-require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
-
-describe DattsRight do
- before do
- reset_database
- @page = Page.create
- end
-
- describe ".add_dynamic_attribute(attr_key, object_type)" do
- it "should be aliased by add_datt" do
- Page.instance_method(:add_dynamic_attribute).should == Page.instance_method(:add_datt)
- end
-
- it "should add a dynamic attribute" do
- @page.add_dynamic_attribute(:rocks, "string")
- @page.dynamic_attribute_details(:rocks).value.should be_nil
- @page.add_dynamic_attribute(:rock, "string", "123")
- @page.read_datt(:rock).should == "123"
- @page.dynamic_attribute_details(:rock).value.should == "123"
- end
-
- it "should ignore when trying to add same attribute" do
- @page.add_dynamic_attribute(:rocks, "string")
- @page.add_dynamic_attribute(:rocks, "integer")
- @page.dynamic_attribute_details(:rocks).object_type.should == "string"
- end
-
- it "should return false if the method that is being added already exists" do
- @page.add_dynamic_attribute(:name, "text").should be_false
- end
-
- it "should not make any changes to the original attribute" do
- @page.update_attribute(:name, "juno")
- @page.add_dynamic_attribute(:name, "text")
- @page.name.should == "juno"
- end
- end
-
- describe ".add_dynamic_attribute!" do
- it "should be aliased by add_datt!" do
- Page.instance_method(:add_dynamic_attribute!).should == Page.instance_method(:add_datt!)
- end
-
- it "should create the dynamic attribute, not waiting to save the attributable record" do
- @page.add_dynamic_attribute!(:price, "integer", 200)
- Page.last.price.should == 200
- end
- end
-end
-
View
29 spec/datts_right/inherit_definition_spec.rb
@@ -0,0 +1,29 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe DattsRight do
+ before do
+ reset_database
+ @category = Category.create
+ @page = Page.create :category => @category
+ @category.definition = {:teaser => {:object_type => "text"}, :price => {:object_type => "integer"}}
+ @category.save
+ end
+
+ describe "#inherit_definition!" do
+ it "should add and create the definitions" do
+ @page.inherit_definition!
+ @page.reload
+ @page.dynamic_attributes.find_by_attr_key(:teaser).should_not be_new_record
+ @page.dynamic_attributes.find_by_attr_key(:price).should_not be_new_record
+ end
+ end
+
+ describe "#inherit_definition" do
+ it "should add but not create the definitions" do
+ @page.inherit_definition
+ @page.reload
+ @page.dynamic_attributes.find_by_attr_key(:teaser).should be_nil
+ @page.dynamic_attributes.find_by_attr_key(:price).should be_nil
+ end
+ end
+end
View
7 spec/datts_right/sync_definition_spec.rb
@@ -16,6 +16,13 @@
@page.dynamic_attribute_details(:price)[:object_type].should == "integer"
end
+ it "should save the new dynamic attributes" do
+ @category.add_definition(:price, {:object_type => "integer"})
+ @category.save
+ @page.should_receive(:save)
+ @page.sync_definition!
+ end
+
it "should remove any dynamic attributes if the definitions are removed" do
@category.remove_definition(:body)
@category.save
View
4 spec/datts_right/update_definition_spec.rb
@@ -21,10 +21,10 @@
c.definition[:bodi].should == {:object_type => "string"}
end
- it "should not raise AlreadyDefined if the attr_key is the same" do
+ it "should not raise AlreadyDefinedError if the attr_key is the same" do
c = Category.create
c.definition = {:robot => {:object_type => "string"}}
- lambda {c.update_definition(:robot, :attr_key => "robot")}.should_not raise_error(DattsRight::AlreadyDefined)
+ lambda {c.update_definition(:robot, :attr_key => "robot")}.should_not raise_error(DattsRight::AlreadyDefinedError)
end
it "should be able to update the rest (all except attr_key) of the definition" do
View
0  lib/datts_right/category.rb → spec/fixtures/category.rb
File renamed without changes
View
0  ...atts_right/category_without_reflection.rb → spec/fixtures/category_without_reflection.rb
File renamed without changes
View
0  lib/datts_right/page.rb → spec/fixtures/page.rb
File renamed without changes
View
0  lib/datts_right/page_without_reflection.rb → spec/fixtures/page_without_reflection.rb
File renamed without changes
View
8 spec/spec_helper.rb
@@ -5,10 +5,10 @@
require 'active_record/errors'
require 'rspec'
require 'datts_right'
-require 'datts_right/page'
-require 'datts_right/page_without_reflection'
-require 'datts_right/category'
-require 'datts_right/category_without_reflection'
+require 'spec/fixtures/page'
+require 'spec/fixtures/page_without_reflection'
+require 'spec/fixtures/category'
+require 'spec/fixtures/category_without_reflection'
# Allow to connect to SQLite
root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
Please sign in to comment.
Something went wrong with that request. Please try again.