Skip to content

Instantly share code, notes, and snippets.

@thedanielhanke
Forked from iblue/embed.rb
Created November 28, 2017 19:23
Show Gist options
  • Select an option

  • Save thedanielhanke/9035f6cf564009e719e766b045cc1d33 to your computer and use it in GitHub Desktop.

Select an option

Save thedanielhanke/9035f6cf564009e719e766b045cc1d33 to your computer and use it in GitHub Desktop.

Revisions

  1. @iblue iblue revised this gist Jul 8, 2012. 2 changed files with 83 additions and 142 deletions.
    155 changes: 83 additions & 72 deletions embed.rb
    Original 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 definitions
    # class ColorPalette < ActiveRecord::Base; embeds_many :colors; end
    # class Color < ActiveRecord::Base; end
    # @example
    # class Invoice
    # include ActiveRecord::Embedding
    #
    # @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
    # embeds_many :items
    # end
    #
    # @author Michael Kessler
    # modified by Markus Fenske <[email protected]>
    #
    module Embed
    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

    module InstanceMethods
    # Sets the attributes
    #
    # @param new_attributes [Hash] the new attributes
    #
    def attributes=(attrs)
    return unless attrs.is_a?(Hash)

    # 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)
    # Create a copy early so we do not overwrite the argument
    new_attributes = attrs.dup

    self.class.embeddings.each do |embed|
    if new_attributes[embed]
    new_attributes["#{ embed }_attributes"] = new_attributes[embed]
    new_attributes.delete(embed)
    end
    end
    mark_for_destruction(new_attributes)

    super(new_attributes, guard_protected_attributes)
    self.class.embeddings.each do |embed|
    if new_attributes[embed]
    new_attributes["#{embed}_attributes"] = new_attributes[embed]
    new_attributes.delete(embed)
    end
    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
    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
    # 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

    private
    # 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

    # 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
    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

    attributes
    end

    attributes
    end
    end
    end
    70 changes: 0 additions & 70 deletions embed_spec.rb
    Original file line number Diff line number Diff line change
    @@ -1,70 +0,0 @@
    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
  2. Michael Kessler revised this gist Apr 8, 2011. 3 changed files with 211 additions and 71 deletions.
    141 changes: 141 additions & 0 deletions embed.rb
    Original 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
    70 changes: 70 additions & 0 deletions embed_spec.rb
    Original 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
    71 changes: 0 additions & 71 deletions embedding.rb
    Original file line number Diff line number Diff line change
    @@ -1,71 +0,0 @@
    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
    # - Embedded documents appears in the JSON output
    #
    # @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
  3. Michael Kessler revised this gist Mar 29, 2011. 1 changed file with 1 addition and 3 deletions.
    4 changes: 1 addition & 3 deletions embedding.rb
    Original 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
    # - Updates attributes marks missing children for destruction without
    # supplying the normal `_destroy` parameter.
    # - Embedded documents appears in the JSON
    # - Embedded documents appears in the JSON output
    #
    # @author Michael Kessler
    #
  4. Michael Kessler created this gist Mar 29, 2011.
    73 changes: 73 additions & 0 deletions embedding.rb
    Original 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