datts_right 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,10 @@
1
+ module DattsRight
2
+ module Exceptions
3
+ class AttributeExistsError < StandardError
4
+ attr_reader :reason
5
+ def initialize(reason)
6
+ @reason = reason
7
+ end
8
+ end
9
+ end
10
+ 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,3 @@
1
+ class Page < ActiveRecord::Base
2
+ has_datts
3
+ 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
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ rails generate migration Thing
6
+
7
+ This will create:
8
+ what/will/it/create
@@ -0,0 +1,3 @@
1
+ class MigrationGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path('../templates', __FILE__)
3
+ end
@@ -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