datts_right 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +5 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +108 -0
- data/LICENSE.txt +20 -0
- data/README.textile +178 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/datts_right.gemspec +236 -0
- data/lib/datts_right.rb +98 -0
- data/lib/datts_right/datt.rb +40 -0
- data/lib/datts_right/exceptions.rb +10 -0
- data/lib/datts_right/instance_methods.rb +214 -0
- data/lib/datts_right/page.rb +3 -0
- data/lib/datts_right/query_methods.rb +15 -0
- data/lib/generators/migration/USAGE +8 -0
- data/lib/generators/migration/migration_generator.rb +3 -0
- data/spec/datt_spec.rb +20 -0
- data/spec/datts_right_spec.rb +299 -0
- data/spec/has_datts_migration_generator_spec.rb +13 -0
- data/spec/spec_helper.rb +43 -0
- metadata +948 -0
data/lib/datts_right.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'datts_right/instance_methods'
|
2
|
+
require 'datts_right/datt'
|
3
|
+
#require 'datts_right/query_methods'
|
4
|
+
#require 'datts_right/exceptions'
|
5
|
+
|
6
|
+
module DattsRight
|
7
|
+
def has_datts(options={})
|
8
|
+
include DattsRight::InstanceMethods
|
9
|
+
#include DattsRight::QueryMethods
|
10
|
+
#ActiveRecord::QueryMethods.extend DattsRight::QueryMethods
|
11
|
+
#include DattsRight::Exceptions
|
12
|
+
|
13
|
+
cattr_accessor :datts_options
|
14
|
+
self.datts_options = options
|
15
|
+
|
16
|
+
has_many :datts, :as => :attributable, :dependent => :destroy
|
17
|
+
|
18
|
+
# Carry out delayed actions before save
|
19
|
+
before_save :build_dynamic_attributes
|
20
|
+
|
21
|
+
# scope :scope_self when looking through attributes so we don't look through all datts
|
22
|
+
# Why? What if you have Friend and Page models.
|
23
|
+
# * Some Phone records have a datt :price
|
24
|
+
# * Some Page records have a datt :price
|
25
|
+
#
|
26
|
+
# When we do Page.find_by_price(400) we want to search only the datts that belong to Page
|
27
|
+
# and we want to disregard the rest of the datts.
|
28
|
+
scope :scope_self, lambda { joins(:datts).where("datts.attributable_type = :klass", :klass => self.name) }
|
29
|
+
scope :dattr_key, lambda { |attr_key| scope_self.joins(:datts).where("datts.attr_key = :attr_key", :attr_key => attr_key)}
|
30
|
+
scope :dattr_type, lambda { |object_type| scope_self.joins(:datts).where("object_type = :object_type", :object_type => object_type) }
|
31
|
+
|
32
|
+
scope :order_datt, lambda { |attr_key_with_order, object_type|
|
33
|
+
# possible attr_key_with_order forms: "field_name", "field_name ASC", "field_name DESC"
|
34
|
+
split_attr_key_with_order = attr_key_with_order.split(" ")
|
35
|
+
attr_key = split_attr_key_with_order.first
|
36
|
+
order_by = split_attr_key_with_order.last if split_attr_key_with_order.size > 1
|
37
|
+
order_value = "datts.#{object_type}_value"
|
38
|
+
order_value << " #{order_by}" if order_by
|
39
|
+
scope_self.dattr_key(attr_key).joins(:datts).dattr_type(object_type).order(order_value)
|
40
|
+
}
|
41
|
+
|
42
|
+
scope :where_datt, lambda { |opts|
|
43
|
+
# TODO accept stuff other than the normal hash
|
44
|
+
# Lifted from AR::Relation#build_where
|
45
|
+
attributes = case opts
|
46
|
+
when String, Array
|
47
|
+
self.expand_hash_conditions_for_aggregates(opts)
|
48
|
+
when Hash
|
49
|
+
opts
|
50
|
+
end
|
51
|
+
results = self#joins(:datts)
|
52
|
+
# Look for all DattsRight records whose:
|
53
|
+
# 1. Has all dynamic_attributes
|
54
|
+
# 2. Those dynamic attributes match there conditions
|
55
|
+
attributes.each do |k, v|
|
56
|
+
conditions = "exists (" +
|
57
|
+
"select 1 from datts datt where " +
|
58
|
+
# restricting the attributable_id to pages.id forces all the exists clauses to reference a single attributable record. Layman's terms, assuming Page uses datts_right:
|
59
|
+
# find only pages that has the matching dynamic attributes
|
60
|
+
"#{self.table_name}.id = datt.attributable_id " +
|
61
|
+
"and datt.attributable_type = :attributable_type " +
|
62
|
+
"and datt.name = :name and datt.#{Datt.attr_column(v)} = :value" +
|
63
|
+
")"
|
64
|
+
results = results.where(conditions, :attributable_type => self.name, :name => k.to_s, :value => v)
|
65
|
+
end
|
66
|
+
results
|
67
|
+
}
|
68
|
+
|
69
|
+
def self.method_missing(method_id, *arguments)
|
70
|
+
# TODO better way to hook this into the rails code, and not define my own
|
71
|
+
if method_id.to_s =~ /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/
|
72
|
+
attributes = $2.split("_and_")
|
73
|
+
#puts "Will find by #{attributes.inspect}"
|
74
|
+
arg = arguments.first
|
75
|
+
scope_self.joins(:datts).where("datts.#{Datt.attr_column(arg)} = :value", :value => arg)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Override AR::Base#respond_to? so we can return the matchers even if the
|
80
|
+
# attribute doesn't exist in the actual columns. Is this expensive?
|
81
|
+
def respond_to?(method_id, include_private=false)
|
82
|
+
# TODO perhaps we could save a cache somewhere of all methods used by
|
83
|
+
# any of the records of this class. that way, we can make this method
|
84
|
+
# act a bit more like AR::Base#respond_to?
|
85
|
+
# Ex:
|
86
|
+
# return true if all_attributes_exists?(match.attribute_names) || all_dynamic_attributes_exists?(match.attribute_names)
|
87
|
+
if match = ActiveRecord::DynamicFinderMatch.match(method_id)
|
88
|
+
return true
|
89
|
+
elsif match = ActiveRecord::DynamicScopeMatch.match(method_id)
|
90
|
+
return true
|
91
|
+
end
|
92
|
+
|
93
|
+
super
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
ActiveRecord::Base.extend DattsRight
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Datt < ActiveRecord::Base
|
2
|
+
# TODO
|
3
|
+
# Set table name from configuration
|
4
|
+
belongs_to :attributable, :polymorphic => true
|
5
|
+
|
6
|
+
def value
|
7
|
+
send("#{object_type}_value")
|
8
|
+
end
|
9
|
+
|
10
|
+
def value=(v)
|
11
|
+
object_type = self.class.attr_type?(v)
|
12
|
+
|
13
|
+
# only allow object_type changes if this is NOT a new record
|
14
|
+
# and it's text to string or vice versa
|
15
|
+
interchangeable_types = %w(text string)
|
16
|
+
#puts "It's currently a #{self.object_type}, and we wanna make it a #{object_type}"
|
17
|
+
if !new_record? && interchangeable_types.include?(self.object_type) && interchangeable_types.include?(object_type) && self.object_type != object_type
|
18
|
+
#puts "It's not new, and we're setting it to #{object_type}"
|
19
|
+
self.object_type = object_type
|
20
|
+
end
|
21
|
+
send("#{self.class.attr_column(v)}=", v)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.attr_type?(v)
|
25
|
+
object_type = if v.is_a?(Integer)
|
26
|
+
"integer"
|
27
|
+
elsif v.is_a?(Float)
|
28
|
+
"float"
|
29
|
+
elsif v.is_a?(TrueClass) || v.is_a?(FalseClass)
|
30
|
+
"boolean"
|
31
|
+
elsif v.is_a?(String)
|
32
|
+
v.size < 255 ? "string" : "text"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns what column this kind of data should be in
|
37
|
+
def self.attr_column(v)
|
38
|
+
"#{self.attr_type?(v)}_value"
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
module DattsRight
|
2
|
+
module InstanceMethods
|
3
|
+
def add_dynamic_attribute(name, klass)
|
4
|
+
unless attributes.keys.include?(name.to_s)
|
5
|
+
name = name.to_s
|
6
|
+
datt = datts.find_by_attr_key(name.underscore)
|
7
|
+
unless datt
|
8
|
+
datt = datts.create :name => name, :attr_key => name.underscore, :object_type => klass
|
9
|
+
dynamic_columns(true)
|
10
|
+
end
|
11
|
+
#puts "Just added #{name} to #{self.class.name}##{id}, and here are the dynamic_attributes: #{dynamic_columns}"
|
12
|
+
datt
|
13
|
+
else
|
14
|
+
false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def remove_dynamic_attribute(name)
|
19
|
+
name = name.to_s
|
20
|
+
datt = datts.find_by_attr_key(name)
|
21
|
+
if datt
|
22
|
+
datt.destroy
|
23
|
+
dynamic_columns(true)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Works like ActiveRecord's attributes except it returns dynamic attributes only
|
28
|
+
def dynamic_attributes(reload=false)
|
29
|
+
@dynamic_attributes ||= {}
|
30
|
+
return @dynamic_attributes if !reload
|
31
|
+
#puts "Reloading dynamic_attributes"
|
32
|
+
datts.each do |datt|
|
33
|
+
#puts "Adding this to @dynamic_attributes: #{datt.attr_key} => #{datt.value}"
|
34
|
+
@dynamic_attributes.merge({datt.attr_key => datt.value})
|
35
|
+
end
|
36
|
+
#puts "Here are teh datts: #{datts.inspect}, and this is what we're returning: #{@dynamic_attributes}"
|
37
|
+
@dynamic_attributes.symbolize_keys!
|
38
|
+
end
|
39
|
+
|
40
|
+
# Because we cannot determine the difference between the inexistence
|
41
|
+
# between key value pair point to nil (eg :hi => nil) and
|
42
|
+
# the key not existing, we need to have a different method to return
|
43
|
+
# information about the record in this format:
|
44
|
+
# {:price => {:object_type => "integer", :value => 200}}
|
45
|
+
def dynamic_columns(reload=false)
|
46
|
+
@dynamic_columns ||= {}
|
47
|
+
@dynamic_columns if !reload
|
48
|
+
#puts "Building the dynamic_columns cache, and we'll look through #{datts.count} datts"
|
49
|
+
@dynamic_columns={}
|
50
|
+
datts.reload
|
51
|
+
datts.each do |datt|
|
52
|
+
#puts "Going through datt##{datt.id}"
|
53
|
+
dynamic_column = {
|
54
|
+
datt.attr_key.to_sym => {
|
55
|
+
:object_type => datt.object_type,
|
56
|
+
:value => datt.value
|
57
|
+
}
|
58
|
+
}
|
59
|
+
#puts "Added #{dynamic_column} to the dynamic columns"
|
60
|
+
@dynamic_columns.merge!(dynamic_column)
|
61
|
+
end
|
62
|
+
#puts "in dynamic_columns, returning: #{@dynamic_columns.inspect}"
|
63
|
+
@dynamic_columns
|
64
|
+
end
|
65
|
+
|
66
|
+
# Determines if the given attribute is a dynamic attribute.
|
67
|
+
def dynamic_attribute?(attr)
|
68
|
+
#puts "in dynamic_attribute?(:#{attr}). datts: #{datts.inspect}. Datt: #{Datt.all.inspect}"
|
69
|
+
#dynamic_columns.include?(attr.to_s)
|
70
|
+
!dynamic_columns[attr.to_sym].nil?
|
71
|
+
#return dynamic_attributes.include?(attr.to_sym) unless dynamic_attributes.empty?
|
72
|
+
#return false if self.class.column_names.include?(attr.to_s)
|
73
|
+
#false
|
74
|
+
end
|
75
|
+
|
76
|
+
# This overrides the attributes= defined in ActiveRecord::Base
|
77
|
+
# The only difference is that this doesn't check to see if the
|
78
|
+
# model responds_to the method before sending it
|
79
|
+
#def attributes=(new_attributes, guard_protected_attributes = true)
|
80
|
+
#return unless new_attributes.is_a?(Hash)
|
81
|
+
#attributes = new_attributes.stringify_keys
|
82
|
+
|
83
|
+
#multi_parameter_attributes = []
|
84
|
+
#attributes = sanitize_for_mass_assignment(attributes) if guard_protected_attributes
|
85
|
+
|
86
|
+
#attributes.each do |k, v|
|
87
|
+
#if k.include?("(")
|
88
|
+
#multi_parameter_attributes << [ k, v ]
|
89
|
+
#else
|
90
|
+
#send("#{k}=", v)
|
91
|
+
#end
|
92
|
+
#end
|
93
|
+
|
94
|
+
#assign_multiparameter_attributes(multi_parameter_attributes)
|
95
|
+
#end
|
96
|
+
|
97
|
+
# Overrides AR::Persistence#update_attributes
|
98
|
+
# Needed to change it because the AR::Persistence method updated the attributes
|
99
|
+
# method directly.
|
100
|
+
# Ex.
|
101
|
+
# self.attributes = attributes
|
102
|
+
#
|
103
|
+
# That won't work because our dynamic attributes aren't in the attributes field
|
104
|
+
def update_attributes(attributes)
|
105
|
+
# The following transaction covers any possible database side-effects of the
|
106
|
+
# attributes assignment. For example, setting the IDs of a child collection.
|
107
|
+
with_transaction_returning_status do
|
108
|
+
attributes.each do |k, v|
|
109
|
+
self.send("#{k}=", v)
|
110
|
+
end
|
111
|
+
save
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Overrides AR::Persistence#update_attributes!
|
116
|
+
# See DattsRight#update_attributes for the reason why
|
117
|
+
def update_attributes!(attributes)
|
118
|
+
with_transaction_returning_status do
|
119
|
+
attributes.each do |k, v|
|
120
|
+
self.send("#{k}=", v)
|
121
|
+
end
|
122
|
+
save!
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Called after validation on update so that dynamic attributes behave
|
129
|
+
# like normal attributes in the fact that the database is not touched
|
130
|
+
# until save is called.
|
131
|
+
def build_dynamic_attributes
|
132
|
+
return if @save_dynamic_attr.nil?
|
133
|
+
# This originally saves things into the "datts" column.
|
134
|
+
#write_attribute_without_dynamic_attributes "datts", @save_dynamic_attr
|
135
|
+
# We save things into the datts table
|
136
|
+
@save_dynamic_attr.each do |k, v|
|
137
|
+
k = k.to_s
|
138
|
+
datt = datts.find_by_attr_key(k)
|
139
|
+
#puts "to create or to update #{k}?"
|
140
|
+
if datt
|
141
|
+
datt.update_attribute(:value, v)
|
142
|
+
#puts "updated: #{datt.inspect}"
|
143
|
+
#else
|
144
|
+
#datt = datts.create! :name => k, :attr_key => k, :value => v
|
145
|
+
#puts "created: #{datt.inspect} among these datts: #{datts.inspect}"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
@save_dynamic_attr = {}
|
149
|
+
true
|
150
|
+
end
|
151
|
+
|
152
|
+
# Implements dynamic-attributes as if real getter/setter methods
|
153
|
+
# were defined.
|
154
|
+
def method_missing(method_id, *args, &block)
|
155
|
+
#puts "in method_missing_without_dynamic_attributes. Checking #{method_id}"
|
156
|
+
begin
|
157
|
+
super method_id, *args, &block
|
158
|
+
rescue NoMethodError => e
|
159
|
+
attr_name = method_id.to_s.sub(/\=$/, '')
|
160
|
+
#puts "Now we check to see if price is a dynamic attribute. dynamic_columns: #{dynamic_columns}. is #{attr_name} in those dynamic_columns? #{dynamic_attribute?(attr_name)}"
|
161
|
+
if dynamic_attribute?(attr_name)
|
162
|
+
#puts "== And #{method_id} is a dynamic method"
|
163
|
+
if method_id.to_s =~ /\=$/
|
164
|
+
return write_attribute(attr_name, args[0])
|
165
|
+
else
|
166
|
+
return read_attribute(attr_name)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
raise e
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Overrides AR::Base#read_attribute
|
174
|
+
def read_attribute(attr_name)
|
175
|
+
attr_name = attr_name.to_s
|
176
|
+
if dynamic_attribute?(attr_name)
|
177
|
+
#puts "trying to read datt #{attr_name}"
|
178
|
+
# If value is in the @save_dynamic_attr cache, and it's not blank
|
179
|
+
if !@save_dynamic_attr.blank? and @save_dynamic_attr[attr_name]
|
180
|
+
#puts "datt #{attr_name} IS in cache, so let's return its value"
|
181
|
+
# Then we return the value
|
182
|
+
return @save_dynamic_attr[attr_name]
|
183
|
+
else # The value is not there, or it's blank
|
184
|
+
#puts "datt #{attr_name} is NOT in cache, so let's load it from the datt table"
|
185
|
+
#attrs = read_attribute_without_dynamic_attributes(dynamic_attributes_options[:column_name].to_s)
|
186
|
+
#puts "Here are ALL the datts: #{Datt.all.inspect} and my id is: #{id}"
|
187
|
+
#puts "Here are my the datts: #{datts.inspect} and my id is: #{id}. and the first datt's value is #{datts.first.value}"
|
188
|
+
datt = datts.find_by_attr_key(attr_name)
|
189
|
+
#puts "This is the dat I find: #{datt.inspect}"
|
190
|
+
return datt.try(:value)
|
191
|
+
#attrs = attrs.nil? ? nil : YAML.load(attrs).symbolize_keys! unless attrs.is_a? Hash
|
192
|
+
#return nil if attrs.blank?
|
193
|
+
#return attrs[attr_name.to_sym]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
super(attr_name)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Overrides AR::Base#write_attribute
|
201
|
+
def write_attribute(attr_name, value)
|
202
|
+
#puts "attempting to write: #{attr_name} = #{value}"
|
203
|
+
if dynamic_attribute?(attr_name)
|
204
|
+
#puts "#{attr_name} is a dynamic_attribute"
|
205
|
+
attr_name = attr_name.to_s
|
206
|
+
|
207
|
+
@save_dynamic_attr ||= {} # create the cache if needed
|
208
|
+
return @save_dynamic_attr[attr_name] = value
|
209
|
+
end
|
210
|
+
|
211
|
+
super(attr_name, value)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module DattsRight
|
2
|
+
module QueryMethods
|
3
|
+
puts "in DattsRight::QueryMethods"
|
4
|
+
def order(*args)
|
5
|
+
puts "In DattsRight::QueryMethods#order with these args: #{args}"
|
6
|
+
relation = clone
|
7
|
+
puts "= This is the relation: #{relation.inspect}"
|
8
|
+
relation.order_values += args.flatten unless args.blank?
|
9
|
+
puts "= This is the relation.order_values: #{relation.order_values.inspect}"
|
10
|
+
relation
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveRecord::QueryMethods.extend DattsRight::QueryMethods
|
data/spec/datt_spec.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe Datt do
|
4
|
+
before do
|
5
|
+
reset_database
|
6
|
+
@page = Page.create
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should allow transferring between String and Text" do
|
10
|
+
@page.add_dynamic_attribute(:foo, "string")
|
11
|
+
@page.foo = "this is a string"
|
12
|
+
@page.save
|
13
|
+
@page.foo = "t"*300
|
14
|
+
@page.save
|
15
|
+
@page.datts.find_by_attr_key("foo").object_type.should == "text"
|
16
|
+
@page.foo = "this is a string again"
|
17
|
+
@page.save
|
18
|
+
@page.datts.find_by_attr_key("foo").object_type.should == "string"
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,299 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe DattsRight do
|
4
|
+
before do
|
5
|
+
reset_database
|
6
|
+
@page = Page.create
|
7
|
+
end
|
8
|
+
|
9
|
+
describe ".add_dynamic_attribute(attr_key, object_type)" do
|
10
|
+
it "should add a dynamic attribute with nil value" do
|
11
|
+
@page.add_dynamic_attribute(:rocks, "string")
|
12
|
+
@page.dynamic_attributes[:rocks].should be_nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should ignore when trying to add same attribute" do
|
16
|
+
@page.add_dynamic_attribute(:rocks, "string")
|
17
|
+
@page.add_dynamic_attribute(:rocks, "integer")
|
18
|
+
@page.dynamic_columns[:rocks][:object_type].should == "string"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should return false if the method that is being added already exists" do
|
22
|
+
@page.add_dynamic_attribute(:name, "text").should be_false
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should not make any changes to the original attribute" do
|
26
|
+
@page.update_attribute(:name, "juno")
|
27
|
+
@page.add_dynamic_attribute(:name, "text").should be_false
|
28
|
+
@page.name.should == "juno"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe ".remove_dynamic_attribute(attr_key)" do
|
33
|
+
it "should remove the attribute completely" do
|
34
|
+
@page.add_dynamic_attribute(:rocks, "string")
|
35
|
+
@page.remove_dynamic_attribute(:rocks)
|
36
|
+
lambda { @page.rocks }.should raise_error(NoMethodError)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should not explode if the attribute being removed isn't there" do
|
40
|
+
@page.add_dynamic_attribute(:rocks, "string")
|
41
|
+
@page.remove_dynamic_attribute(:rocks)
|
42
|
+
lambda {
|
43
|
+
@page.remove_dynamic_attribute(:rocks)
|
44
|
+
}.should_not raise_error(NoMethodError)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe ".dynamic_columns(reload)" do
|
49
|
+
it "should return an array of symbols of the available dynamic columns" do
|
50
|
+
@page.add_dynamic_attribute(:rocks, "string")
|
51
|
+
@page.is_dynamic_attribute?(:rocks).should be_true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should not allow arbitrary creation of attributes" do
|
56
|
+
lambda {
|
57
|
+
@page.some_field = "woot"
|
58
|
+
}.should raise_error(NoMethodError)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should allow saving of integers" do
|
62
|
+
@page.add_dynamic_attribute(:price, "integer")
|
63
|
+
@page.price = 300
|
64
|
+
@page.save
|
65
|
+
@page = Page.last
|
66
|
+
@page.price.should == 300
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should allow saving of floats" do
|
70
|
+
@page.add_dynamic_attribute(:price, "float")
|
71
|
+
@page.price = 300.0
|
72
|
+
@page.save
|
73
|
+
@page = Page.last
|
74
|
+
@page.price.should == 300.0
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should allow saving of true" do
|
78
|
+
@page.add_dynamic_attribute(:real, "boolean")
|
79
|
+
@page.real = true
|
80
|
+
@page.save
|
81
|
+
@page = Page.last
|
82
|
+
@page.real.should be_true
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should allow saving of false" do
|
86
|
+
@page.add_dynamic_attribute(:real, "boolean")
|
87
|
+
@page.real = false
|
88
|
+
@page.save
|
89
|
+
@page = Page.last
|
90
|
+
@page.real.should be_false
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should allow saving of strings" do
|
94
|
+
@page.add_dynamic_attribute(:real, "string")
|
95
|
+
@page.real = "its real alright"
|
96
|
+
@page.save
|
97
|
+
@page = Page.last
|
98
|
+
@page.real.should == "its real alright"
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should allow saving of text" do
|
102
|
+
@page.add_dynamic_attribute(:real, "text")
|
103
|
+
@page.real = "t"*256
|
104
|
+
@page.save
|
105
|
+
@page = Page.last
|
106
|
+
@page.real.should == "t"*256
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should cache the attribute" do
|
110
|
+
@page.add_dynamic_attribute(:price, "integer")
|
111
|
+
@page.price = 200
|
112
|
+
@page.price.should == 200
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should not save the attribute if save on the attributable object isn't called" do
|
116
|
+
@page.add_dynamic_attribute(:price, "integer")
|
117
|
+
@page.price = 200
|
118
|
+
@page = Page.last
|
119
|
+
@page.price.should be_nil
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should allow overwriting of values" do
|
123
|
+
@page.add_dynamic_attribute(:price, "integer")
|
124
|
+
@page.price = 200
|
125
|
+
@page.save
|
126
|
+
@page = Page.last
|
127
|
+
@page.price = 300
|
128
|
+
@page.save
|
129
|
+
@page = Page.last
|
130
|
+
@page.price.should == 300
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should allow nullifying of attributes, but keeping the fields there" do
|
134
|
+
@page.add_dynamic_attribute(:farce, "string")
|
135
|
+
@page.farce = "Nothing here my friend"
|
136
|
+
@page.save
|
137
|
+
@page.farce = nil
|
138
|
+
@page.is_dynamic_attribute?(:farce).should be_true
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "on dynamic find_by methods" do
|
142
|
+
it "should work" do
|
143
|
+
@page.add_dynamic_attribute(:price, "integer")
|
144
|
+
@page.price = 400
|
145
|
+
@page.save
|
146
|
+
|
147
|
+
@page_2 = Page.create
|
148
|
+
@page_2.add_dynamic_attribute(:price, "integer")
|
149
|
+
@page_2.price = 500
|
150
|
+
@page_2.save
|
151
|
+
|
152
|
+
Page.find_by_price(400).should include(@page)
|
153
|
+
Page.find_by_price(400).should_not include(@page_2)
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should allow chaining" do
|
157
|
+
@page.add_dynamic_attribute(:price, "integer")
|
158
|
+
@page.name = "fixed"
|
159
|
+
@page.price = 400
|
160
|
+
@page.save
|
161
|
+
|
162
|
+
@page_2 = Page.create
|
163
|
+
@page_2.add_dynamic_attribute(:price, "integer")
|
164
|
+
@page_2.price = 500
|
165
|
+
@page_2.save
|
166
|
+
|
167
|
+
@pages = Page.where(:name => "fixed").find_by_price(400)
|
168
|
+
@pages.should include(@page)
|
169
|
+
@pages.should_not include(@page_2)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
it "should allow multiple attributes" do
|
174
|
+
@page.add_dynamic_attribute(:price, "integer")
|
175
|
+
@page.price = 400
|
176
|
+
@page.add_dynamic_attribute(:farce, "string")
|
177
|
+
@page.farce = "hi"
|
178
|
+
@page.save
|
179
|
+
|
180
|
+
@page_2 = Page.create
|
181
|
+
@page_2.add_dynamic_attribute(:price, "integer")
|
182
|
+
@page_2.price = 400
|
183
|
+
@page_2.add_dynamic_attribute(:farce, "string")
|
184
|
+
@page_2.farce = "hitt"
|
185
|
+
@page_2.save
|
186
|
+
|
187
|
+
@pages = Page.find_by_farce_and_price("hi", 400)
|
188
|
+
@pages.should include(@page)
|
189
|
+
@pages.should_not include(@page_2)
|
190
|
+
end
|
191
|
+
|
192
|
+
describe "when the value being assigned is not of the same type (with exceptions)" do
|
193
|
+
before do
|
194
|
+
@page.add_dynamic_attribute(:price, "integer")
|
195
|
+
end
|
196
|
+
|
197
|
+
it "should not save the value" do
|
198
|
+
@page.price = 200
|
199
|
+
@page.save
|
200
|
+
@page.price = "hi there"
|
201
|
+
@page.save
|
202
|
+
Page.last.price.should == 200
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
it "should allow mass assignment" do
|
207
|
+
@page.add_dynamic_attribute(:price, "integer")
|
208
|
+
@page.add_dynamic_attribute(:farce, "string")
|
209
|
+
@page.update_attributes(:price => 200, :farce => "hi")
|
210
|
+
@page.price.should == 200
|
211
|
+
@page.farce.should == "hi"
|
212
|
+
|
213
|
+
@page.update_attributes!(:price => 300, :farce => "not farce!")
|
214
|
+
@page.price.should == 300
|
215
|
+
@page.farce.should == "not farce!"
|
216
|
+
end
|
217
|
+
|
218
|
+
it "should allow use of update_attribute" do
|
219
|
+
@page.add_dynamic_attribute(:price, "integer")
|
220
|
+
@page.update_attribute(:price, 200)
|
221
|
+
@page.price.should == 200
|
222
|
+
end
|
223
|
+
|
224
|
+
describe "ordering" do
|
225
|
+
it "should default to asc" do
|
226
|
+
@page.add_dynamic_attribute(:price, "integer")
|
227
|
+
@page.price = 2
|
228
|
+
@page.save
|
229
|
+
@page_2 = Page.create
|
230
|
+
@page_2.add_dynamic_attribute(:price, "integer")
|
231
|
+
@page_2.price = 1
|
232
|
+
@page_2.save
|
233
|
+
@page_3 = Page.create
|
234
|
+
@page_3.add_dynamic_attribute(:price, "float")
|
235
|
+
@page_3.price = 3.0
|
236
|
+
@page_3.save
|
237
|
+
@page_4 = Page.create
|
238
|
+
@page_4.add_dynamic_attribute(:price, "float")
|
239
|
+
@page_4.price = 3.5
|
240
|
+
@page_4.save
|
241
|
+
|
242
|
+
# What if different records have price but they are of different object_type?
|
243
|
+
# page_1.price is an "integer" and it's saved in "integer_value" column
|
244
|
+
# page_2.price is a "float" and it's saved in "float_value"
|
245
|
+
#
|
246
|
+
# How would you work on Page.order_datt("price")?
|
247
|
+
#
|
248
|
+
# Answer 1: we could require passing of the object_type in the order method
|
249
|
+
# This way, we know which column to look at => order_datt("price", "integer")
|
250
|
+
Page.order_datt("price", "integer").should == [@page_2, @page]
|
251
|
+
Page.order_datt("price DESC", "float").should == [@page_4, @page_3]
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
describe "where_datt" do
|
256
|
+
it "should automatically join in the datts table" do
|
257
|
+
@page.add_dynamic_attribute(:price, "float")
|
258
|
+
@page.price = 200.0
|
259
|
+
@page.add_dynamic_attribute(:farce, "boolean")
|
260
|
+
@page.farce = true
|
261
|
+
@page.save
|
262
|
+
|
263
|
+
@page_2 = Page.create
|
264
|
+
@page_2.add_dynamic_attribute(:price, "integer")
|
265
|
+
@page_2.add_dynamic_attribute(:farce, "boolean")
|
266
|
+
@page_2.farce = true
|
267
|
+
@page_2.price = 1
|
268
|
+
@page_2.save
|
269
|
+
|
270
|
+
@result = Page.where_datt(:price => 200.0, :farce => true)
|
271
|
+
@result.should include(@page)
|
272
|
+
@result.should_not include(@page_2)
|
273
|
+
|
274
|
+
#@result = Page.where_datt("price < :price", :price => 199.0)
|
275
|
+
#@result.should_not include(@page)
|
276
|
+
#@result.should include(@page_2)
|
277
|
+
end
|
278
|
+
|
279
|
+
it "should allow chaining with where scopes" do
|
280
|
+
@page.add_dynamic_attribute(:price, "integer")
|
281
|
+
@page.name = "aardvark"
|
282
|
+
@page.price = 200
|
283
|
+
@page.save
|
284
|
+
|
285
|
+
@page_2 = Page.create
|
286
|
+
@page_2.add_dynamic_attribute(:price, "integer")
|
287
|
+
@page_2.price = 200
|
288
|
+
@page_2.save
|
289
|
+
|
290
|
+
@results = Page.where_datt(:price => 200).where(:name => "aardvark")
|
291
|
+
@results.should include(@page)
|
292
|
+
@results.should_not include(@page_2)
|
293
|
+
end
|
294
|
+
|
295
|
+
it "should return an empty array if we're looking for a field that doesn't exist" do
|
296
|
+
Page.where_datt(:bogus_field => "none").should be_empty
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|