-
-
Save thedanielhanke/9035f6cf564009e719e766b045cc1d33 to your computer and use it in GitHub Desktop.
Revisions
-
iblue revised this gist
Jul 8, 2012 . 2 changed files with 83 additions and 142 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,5 +1,4 @@ module ActiveRecord # Allows embedding of ActiveRecord models. # # Embedding other ActiveRecord models is a composition of the two @@ -9,31 +8,25 @@ module ActiveRecord # - Mass assignment security allows the embedded attributes # - Embedded models are destroyed with the parent when not appearing in an update again # - Embedded documents appears in the JSON output # - Embedded documents that are deleted are not visible to the parent anymore, but # will be deleted *after* save has been caled # # You have to manually include this module # # @example # class Invoice # include ActiveRecord::Embedding # # embeds_many :items # end # # @author Michael Kessler # modified by Markus Fenske <[email protected]> # module Embedding extend ActiveSupport::Concern module ClassMethods mattr_accessor :embeddings self.embeddings = [] @@ -45,6 +38,25 @@ module ClassMethods def embeds_many(models, options = { }) has_many models, options.merge(:dependent => :destroy, :autosave => true) embed_attribute(models) attr_accessible "#{models}_attributes".to_sym # What is marked for destruction does not evist anymore from # our point of view. FIXME: Really evil hack. alias_method "_super_#{models}".to_sym, models define_method models do # This is an evil hack. Because activerecord uses the items method itself to # find out which items are deleted, we need to act differently if called by # ActiveRecord. So we look at the paths in the Backtrace. If there is # activerecord-3 anywhere there, this is called by AR. This will work until # AR 4.0... if caller(0).select{|x| x =~ /activerecord-3/}.any? return send("_super_#{models}".to_sym) end # Otherwise, when we are called by someone else, we will not return the items # marked for destruction. send("_super_#{models}".to_sym).reject(&:marked_for_destruction?) end end # Embeds many ActiveRecord models which have been referenced @@ -68,74 +80,73 @@ def embed_attribute(name) attr_accessible "#{ name }_attributes".to_sym if _accessible_attributes? self.embeddings << name end end # Sets the attributes # # @param new_attributes [Hash] the new attributes # def attributes=(attrs) return unless attrs.is_a?(Hash) # Create a copy early so we do not overwrite the argument new_attributes = attrs.dup mark_for_destruction(new_attributes) self.class.embeddings.each do |embed| if new_attributes[embed] new_attributes["#{embed}_attributes"] = new_attributes[embed] new_attributes.delete(embed) end end super(new_attributes) end # Update attributes and destroys missing embeds # from the database. # # @params attributes [Hash] the attributes to update # def update_attributes(attributes) super(mark_for_destruction(attributes)) end # Update attributes and destroys missing embeds # from the database. # # @params attributes [Hash] the attributes to update # def update_attributes!(attributes) super(mark_for_destruction(attributes)) end # Add the embedded document in JSON serialization # # @param options [Hash] the rendering options # def as_json(options = { }) super({ :include => self.class.embeddings }.merge(options || { })) end private # Marks missing models as deleted. Writes the changes to the database, # after save has been called. # # @param attributes [Hash] the attributes # def mark_for_destruction(attributes) self.class.embeddings.each do |embed| if attributes[embed] updates = attributes[embed].map { |model| model[:id] }.compact destroy = updates.empty? ? send("_super_#{embed}".to_sym).select(:id) : send("_super_#{embed}".to_sym).select(:id).where('id NOT IN (?)', updates) destroy.each { |model| attributes[embed] << { :id => model.id, :_destroy => '1' } } end end attributes end end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,70 +0,0 @@ -
Michael Kessler revised this gist
Apr 8, 2011 . 3 changed files with 211 additions and 71 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,141 @@ module ActiveRecord # Allows embedding of ActiveRecord models. # # Embedding other ActiveRecord models is a composition of the two # and leads to the following behaviour: # # - Nested attributes are accepted on the parent without the _attributes suffix # - Mass assignment security allows the embedded attributes # - Embedded models are destroyed with the parent when not appearing in an update again # - Embedded documents appears in the JSON output # # @example Class definitions # class ColorPalette < ActiveRecord::Base; embeds_many :colors; end # class Color < ActiveRecord::Base; end # # @example Rails console example # palette = ColorPalette.create(:name => 'Dark', :colors => [{ :red => 0, :green => 0, :blue => 0 }]) # palette.colors.count # 1 # palette.update_attributes(:name => 'Light', :colors => [{ :red => 255, :green => 255, :blue => 255 }]) # palette.colors.count # 1 # palette.update_attributes(:name => 'Medium', :colors => [ # { :id => palette.colors.first.id, :red => 255, :green => 255, :blue => 255 } # { :red => 0, :green => 0, :blue => 0 } # ]) # palette.colors.count # 2 # palette.update_attributes(:name => 'Light', :colors => [{ :red => 255, :green => 255, :blue => 255 }]) # palette.colors.count # 1 # # @author Michael Kessler # module Embed extend ActiveSupport::Concern module ClassMethods mattr_accessor :embeddings self.embeddings = [] # Embeds many ActiveRecord model # # @param models [Symbol] the name of the embedded models # @param options [Hash] the embedding options # def embeds_many(models, options = { }) has_many models, options.merge(:dependent => :destroy, :autosave => true) embed_attribute(models) end # Embeds many ActiveRecord models which have been referenced # with has_many. # # @param models [Symbol] the name of the embedded models # def embeds(models) embed_attribute(models) end private # Makes the child model accessible by accepting nested attributes and # makes the attributes accessible when mass assignment security is enabled. # # @param name [Symbol] the name of the embedded model # def embed_attribute(name) accepts_nested_attributes_for name, :allow_destroy => true attr_accessible "#{ name }_attributes".to_sym if _accessible_attributes? self.embeddings << name end end module InstanceMethods # Sets the attributes # # @param new_attributes [Hash] the new attributes # @param guard_protected_attributes [Boolean] respect the protected attributes # def attributes=(new_attributes, guard_protected_attributes = true) return unless new_attributes.is_a?(Hash) self.class.embeddings.each do |embed| if new_attributes[embed] new_attributes["#{ embed }_attributes"] = new_attributes[embed] new_attributes.delete(embed) end end super(new_attributes, guard_protected_attributes) end # Update attributes and destroys missing embeds # from the database. # # @params attributes [Hash] the attributes to update # def update_attributes(attributes) super(mark_for_destruction(attributes)) end # Update attributes and destroys missing embeds # from the database. # # @params attributes [Hash] the attributes to update # def update_attributes!(attributes) super(mark_for_destruction(attributes)) end # Add the embedded document in JSON serialization # # @param options [Hash] the rendering options # def as_json(options = { }) super({ :include => self.class.embeddings }.merge(options || { })) end private # Destroys all the models that are missing from # the new values. # # @param attributes [Hash] the attributes # def mark_for_destruction(attributes) self.class.embeddings.each do |embed| if attributes[embed] updates = attributes[embed].map { |model| model[:id] }.compact destroy = updates.empty? ? send(embed).select(:id) : send(embed).select(:id).where('id NOT IN (?)', updates) destroy.each { |model| attributes[embed] << { :id => model.id, :_destroy => '1' } } end end attributes end end end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,70 @@ require 'spec_helper' class TestPalette < ActiveRecord::Base include ActiveRecord::Embed establish_connection :adapter => 'sqlite3', :database => ':memory:' connection.execute <<-eosql CREATE TABLE test_palettes ( id integer primary key, name string ) eosql embeds_many :test_colors end class TestColor < ActiveRecord::Base establish_connection :adapter => 'sqlite3', :database => ':memory:' connection.execute <<-eosql CREATE TABLE test_colors ( id integer primary key, test_palette_id integer, red integer, green integer, blue integer ) eosql belongs_to :test_palette end describe ActiveRecord::Embed do let(:palette) { TestPalette.create(:name => 'Colors', :test_colors => [{ :red => 0, :green => 0, :blue => 0 }, { :red => 255, :green => 255, :blue => 255 }]) } it 'creates the model' do palette.should be_persisted end it 'creates the embedded models' do palette.test_colors.count.should eql 2 palette.test_colors.first.should be_persisted palette.test_colors.last.should be_persisted end it 'replaces the embedded models' do color_1 = palette.test_colors.first color_2 = palette.test_colors.last palette.update_attributes(:name => 'Color', :test_colors => [{ :red => 255, :green => 255, :blue => 255 }]) palette.test_colors.count.should eql 1 color_1.should_not be_persisted color_2.should_not be_persisted end it 'updates the embedded models' do color_1 = palette.test_colors.first color_2 = palette.test_colors.last palette.update_attributes(:name => 'Colors', :test_colors => [ { :id => color_1.id, :red => 0, :green => 0, :blue => 255 }, { :id => color_2.id, :red => 255, :green => 255, :blue => 0 } ]) palette.test_colors.count.should eql 2 color_1.should be_persisted color_1.blue.should eql 255 color_2.should be_persisted color_2.blue.should eql 0 end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,71 +0,0 @@ -
Michael Kessler revised this gist
Mar 29, 2011 . 1 changed file with 1 addition and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -10,9 +10,7 @@ module ActiveRecord # - Embedded models are auto saved # - Embedded models are destroyed with the parent # - Mass assignment security allows the attributes # - Embedded documents appears in the JSON output # # @author Michael Kessler # -
Michael Kessler created this gist
Mar 29, 2011 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,73 @@ module ActiveRecord # Allows embedding of a ActiveRecord model. # # Embedding another ActiveRecord models is a composition of the two # and leads to the following behaviour: # # - A reference, either one or many, is added # - Nested attributes are accepted on the parent # - Embedded models are auto saved # - Embedded models are destroyed with the parent # - Mass assignment security allows the attributes # - Updates attributes marks missing children for destruction without # supplying the normal `_destroy` parameter. # - Embedded documents appears in the JSON # # @author Michael Kessler # module Embedding extend ActiveSupport::Concern module ClassMethods mattr_accessor :embeds self.embeds = [] # Embeds many ActiveRecord model # # @param association_id [Symbol] the name of the embedded model # @param options [Hash] the embedding options # def embeds_many(association_id, options = { }) has_many association_id, options.merge(:dependent => :destroy, :autosave => true) add_accessible_children(association_id) end # Embeds one ActiveRecord model # # @param association_id [Symbol] the name of the embedded model # @param options [Hash] the embedding options # def embeds_one(association_id, options = {}) has_one association_id, options.merge(:dependent => :destroy, :autosave => true) add_accessible_children(association_id) end # Makes the child model accessible by accepting nested attributes and # makes the attributes accessible when mass assignment security is enabled. # # @param association_id [Symbol] the name of the embedded model # def add_accessible_children(association_id) accepts_nested_attributes_for association_id, :allow_destroy => true attr_accessible "#{ association_id }_attributes".to_sym if self.read_inheritable_attribute('attr_accessible') self.embeds << association_id end end module InstanceMethods # Add the embedded document in JSON serialization # # @param options [Hash] the rendering options # def as_json(options = {}) super({ :include => self.class.embeds }.merge(options || {})) end end end end