Skip to content

Commit

Permalink
Propagate mass assignment options down to children
Browse files Browse the repository at this point in the history
For cases of setting of embedded children via the parent with mass
assignment options enabled or via nested attributes, the flags are now
propagated through like AR.

[ fix #2440 ]
[ fix #2435 ]
  • Loading branch information
durran committed Oct 7, 2012
1 parent 7582271 commit 71fe904
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 70 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ For instructions on upgrading to newer versions, visit


### Resolved Issues ### Resolved Issues


* \#2440/\#2435 Pass mass assignment options down to children when setting via
nested attributes or embedded documents.

* \#2439 Fixed memory leak in threaded selection of returned fields. * \#2439 Fixed memory leak in threaded selection of returned fields.
(Tim Olsen) (Tim Olsen)


Expand Down
69 changes: 58 additions & 11 deletions lib/mongoid/attributes/processing.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -19,19 +19,41 @@ module Processing
# #
# @since 2.0.0.rc.7 # @since 2.0.0.rc.7
def process_attributes(attrs = nil, role = :default, guard_protected_attributes = true) def process_attributes(attrs = nil, role = :default, guard_protected_attributes = true)
attrs ||= {} with_mass_assignment(role, guard_protected_attributes) do
if !attrs.empty? attrs ||= {}
attrs = sanitize_for_mass_assignment(attrs, role) if guard_protected_attributes if !attrs.empty?
attrs.each_pair do |key, value| attrs = sanitize_for_mass_assignment(attrs, role) if guard_protected_attributes
next if pending_attribute?(key, value) attrs.each_pair do |key, value|
process_attribute(key, value) next if pending_attribute?(key, value)
process_attribute(key, value)
end
end end
yield self if block_given?
process_pending
end end
yield self if block_given?
process_pending
end end


protected private

# Get the current mass assignment options for this model.
#
# @api private
#
# @return [ Hash ] The mass assignment options.
#
# @since 3.0.7
def mass_assignment_options
@mass_assignment_options ||= {}
end

# Set the mass assignment options for the current model.
#
# @api private
#
# @return [ Hash ] The mass assignment options.
#
# @since 3.0.7
attr_writer :mass_assignment_options


# If the key provided is the name of a relation or a nested attribute, we # If the key provided is the name of a relation or a nested attribute, we
# need to wait until all other attributes are set before processing # need to wait until all other attributes are set before processing
Expand Down Expand Up @@ -121,6 +143,8 @@ def process_nested
# @example Process the pending items. # @example Process the pending items.
# document.process_pending # document.process_pending
# #
# @param [ Hash ] options The mass assignment options.
#
# @since 2.0.0.rc.7 # @since 2.0.0.rc.7
def process_pending def process_pending
process_nested and process_relations process_nested and process_relations
Expand All @@ -133,18 +157,41 @@ def process_pending
# @example Process the relations. # @example Process the relations.
# document.process_relations # document.process_relations
# #
# @param [ Hash ] options The mass assignment options.
#
# @since 2.0.0.rc.7 # @since 2.0.0.rc.7
def process_relations def process_relations
pending_relations.each_pair do |name, value| pending_relations.each_pair do |name, value|
metadata = relations[name] metadata = relations[name]
if value.is_a?(Hash) if value.is_a?(Hash)
metadata.nested_builder(value, {}).build(self) metadata.nested_builder(value, {}).build(self, mass_assignment_options)
else else
send("#{name}=", value) send("#{name}=", value)
end end
end end
end end

# Execute the block with the provided mass assignment options set.
#
# @api private
#
# @example Execute with mass assignment.
# model.with_mass_assignment(:default, true)
#
# @param [ Symbol ] role The role.
# @param [ true, false ] guard_protected_attributes To enable mass
# assignment.
#
# @since 3.0.7
def with_mass_assignment(role, guard_protected_attributes)
begin
self.mass_assignment_options =
{ as: role, without_protection: !guard_protected_attributes }
yield
ensure
self.mass_assignment_options = nil
end
end
end end
end end
end end

2 changes: 1 addition & 1 deletion lib/mongoid/factory.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module Factory
# #
# @param [ Class ] klass The class to instantiate from if _type is not present. # @param [ Class ] klass The class to instantiate from if _type is not present.
# @param [ Hash ] attributes The document attributes. # @param [ Hash ] attributes The document attributes.
# @param [ Hash ] optiosn The mass assignment scoping options. # @param [ Hash ] options The mass assignment scoping options.
# #
# @return [ Document ] The instantiated document. # @return [ Document ] The instantiated document.
def build(klass, attributes = nil, options = {}) def build(klass, attributes = nil, options = {})
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/nested_attributes.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def accepts_nested_attributes_for(*args)
autosave(metadata.merge!(autosave: true)) autosave(metadata.merge!(autosave: true))
re_define_method(meth) do |attrs| re_define_method(meth) do |attrs|
_assigning do _assigning do
metadata.nested_builder(attrs, options).build(self) metadata.nested_builder(attrs, options).build(self, mass_assignment_options)
end end
end end
end end
Expand Down
23 changes: 17 additions & 6 deletions lib/mongoid/relations/builders/nested_attributes/many.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@ class Many < NestedBuilder
# many.build(person) # many.build(person)
# #
# @param [ Document ] parent The parent document of the relation. # @param [ Document ] parent The parent document of the relation.
# @param [ Hash ] options The mass assignment options.
# #
# @return [ Array ] The attributes. # @return [ Array ] The attributes.
def build(parent) def build(parent, options = {})
@existing = parent.send(metadata.name) @existing = parent.send(metadata.name)
if over_limit?(attributes) if over_limit?(attributes)
raise Errors::TooManyNestedAttributeRecords.new(existing, options[:limit]) raise Errors::TooManyNestedAttributeRecords.new(existing, options[:limit])
end end
attributes.each do |attrs| attributes.each do |attrs|
if attrs.respond_to?(:with_indifferent_access) if attrs.respond_to?(:with_indifferent_access)
process_attributes(parent, attrs) process_attributes(parent, attrs, options)
else else
process_attributes(parent, attrs[1]) process_attributes(parent, attrs[1], options)
end end
end end
end end
Expand Down Expand Up @@ -85,11 +86,17 @@ def over_limit?(attributes)
# Process each set of attributes one at a time for each potential # Process each set of attributes one at a time for each potential
# new, existing, or ignored document. # new, existing, or ignored document.
# #
# @api private
#
# @example Process the attributes # @example Process the attributes
# builder.process_attributes({ "id" => 1, "street" => "Bond" }) # builder.process_attributes({ "id" => 1, "street" => "Bond" })
# #
# @param [ Document ] parent The parent document.
# @param [ Hash ] attrs The single document attributes to process. # @param [ Hash ] attrs The single document attributes to process.
def process_attributes(parent, attrs) # @param [ Hash ] options the mass assignment options.
#
# @since 2.0.0
def process_attributes(parent, attrs, options)
return if reject?(parent, attrs) return if reject?(parent, attrs)
if id = attrs.extract_id if id = attrs.extract_id
first = existing.first first = existing.first
Expand All @@ -100,10 +107,14 @@ def process_attributes(parent, attrs)
doc.destroy unless doc.embedded? || doc.destroyed? doc.destroy unless doc.embedded? || doc.destroyed?
else else
attrs.delete_id attrs.delete_id
metadata.embedded? ? doc.attributes = attrs : doc.update_attributes(attrs) if metadata.embedded?
doc.assign_attributes(attrs, options)
else
doc.update_attributes(attrs, options)
end
end end
else else
existing.push(Factory.build(metadata.klass, attrs)) unless destroyable?(attrs) existing.push(Factory.build(metadata.klass, attrs, options)) unless destroyable?(attrs)
end end
end end
end end
Expand Down
91 changes: 41 additions & 50 deletions lib/mongoid/relations/builders/nested_attributes/one.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -10,25 +10,27 @@ class One < NestedBuilder
# Builds the relation depending on the attributes and the options # Builds the relation depending on the attributes and the options
# passed to the macro. # passed to the macro.
# #
# This attempts to perform 3 operations, either one of an update of # @example Build a 1-1 nested document.
# the existing relation, a replacement of the relation with a new # one.build(person, as: :admin)
# document, or a removal of the relation.
# #
# Example: # @note This attempts to perform 3 operations, either one of an update of
# the existing relation, a replacement of the relation with a new
# document, or a removal of the relation.
# #
# <tt>one.build(person)</tt> # @param [ Document ] parent The parent document.
# @param [ Hash ] options The mass assignment options.
# #
# Options: # @return [ Document ] The built document.
# #
# parent: The parent document of the relation. # @since 2.0.0
def build(parent) def build(parent, options = {})
return if reject?(parent, attributes) return if reject?(parent, attributes)
@existing = parent.send(metadata.name) @existing = parent.send(metadata.name)
if update? if update?
attributes.delete_id attributes.delete_id
existing.attributes = attributes existing.assign_attributes(attributes, options)
elsif replace? elsif replace?
parent.send(metadata.setter, Factory.build(metadata.klass, attributes)) parent.send(metadata.setter, Factory.build(metadata.klass, attributes, options))
elsif delete? elsif delete?
parent.send(metadata.setter, nil) parent.send(metadata.setter, nil)
end end
Expand All @@ -37,19 +39,14 @@ def build(parent)
# Create the new builder for nested attributes on one-to-one # Create the new builder for nested attributes on one-to-one
# relations. # relations.
# #
# Example: # @example Instantiate the builder.
# One.new(metadata, attributes, options)
# #
# <tt>One.new(metadata, attributes, options)</tt> # @param [ Metadata ] metadata The relation metadata.
# @param [ Hash ] attributes The attributes hash to attempt to set.
# @param [ Hash ] options The options defined.
# #
# Options: # @since 2.0.0
#
# metadata: The relation metadata
# attributes: The attributes hash to attempt to set.
# options: The options defined.
#
# Returns:
#
# A new builder.
def initialize(metadata, attributes, options) def initialize(metadata, attributes, options)
@attributes = attributes.with_indifferent_access @attributes = attributes.with_indifferent_access
@metadata = metadata @metadata = metadata
Expand All @@ -62,70 +59,64 @@ def initialize(metadata, attributes, options)
# Is the id in the attribtues acceptable for allowing an update to # Is the id in the attribtues acceptable for allowing an update to
# the existing relation? # the existing relation?
# #
# Example: # @api private
# #
# <tt>acceptable_id?</tt> # @example Is the id acceptable?
# one.acceptable_id?
# #
# Returns: # @return [ true, false ] If the id part of the logic will allow an update.
# #
# True if the id part of the logic will allow an update. # @since 2.0.0
def acceptable_id? def acceptable_id?
id = convert_id(existing.class, attributes[:id]) id = convert_id(existing.class, attributes[:id])
existing.id == id || id.nil? || (existing.id != id && update_only?) existing.id == id || id.nil? || (existing.id != id && update_only?)
end end


# Can the existing relation be deleted? # Can the existing relation be deleted?
# #
# Example: # @example Can the existing object be deleted?
# one.delete?
# #
# <tt>delete?</tt> # @return [ true, false ] If the relation should be deleted.
# #
# Returns: # @since 2.0.0
#
# True if the relation should be deleted.
def delete? def delete?
destroyable? && !attributes[:id].nil? destroyable? && !attributes[:id].nil?
end end


# Can the existing relation potentially be deleted? # Can the existing relation potentially be destroyed?
#
# Example:
# #
# <tt>destroyable?({ :_destroy => "1" })</tt> # @example Is the object destroyable?
# one.destroyable?({ :_destroy => "1" })
# #
# Options: # @return [ true, false ] If the relation can potentially be
# destroyed.
# #
# attributes: The attributes to pull the flag from. # @since 2.0.0
#
# Returns:
#
# True if the relation can potentially be deleted.
def destroyable? def destroyable?
[ 1, "1", true, "true" ].include?(destroy) && allow_destroy? [ 1, "1", true, "true" ].include?(destroy) && allow_destroy?
end end


# Is the document to be replaced? # Is the document to be replaced?
# #
# Example: # @example Is the document to be replaced?
# one.replace?
# #
# <tt>replace?</tt> # @return [ true, false ] If the document should be replaced.
# #
# Returns: # @since 2.0.0
#
# True if the document should be replaced.
def replace? def replace?
!existing && !destroyable? && !attributes.blank? !existing && !destroyable? && !attributes.blank?
end end


# Should the document be updated? # Should the document be updated?
# #
# Example: # @example Should the document be updated?
# # one.update?
# <tt>update?</tt>
# #
# Returns: # @return [ true, false ] If the object should have its attributes updated.
# #
# True if the object should have its attributes updated. # @since 2.0.0
def update? def update?
existing && !destroyable? && acceptable_id? existing && !destroyable? && acceptable_id?
end end
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/relations/embedded/batchable.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def normalize_docs(docs)
docs.map do |doc| docs.map do |doc|
attributes = { metadata: metadata, _parent: base } attributes = { metadata: metadata, _parent: base }
attributes.merge!(doc) attributes.merge!(doc)
Factory.build(klass, attributes) Factory.build(klass, attributes, base.send(:mass_assignment_options))
end end
else else
docs docs
Expand Down
7 changes: 7 additions & 0 deletions spec/app/models/building.rb
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,7 @@
class Building
include Mongoid::Document
attr_accessible
attr_accessible :building_address, :contractors, as: :admin
embeds_one :building_address
embeds_many :contractors
end
7 changes: 7 additions & 0 deletions spec/app/models/building_address.rb
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,7 @@
class BuildingAddress
include Mongoid::Document
attr_accessible
attr_accessible :city, as: :admin
embedded_in :building
field :city, type: String
end
7 changes: 7 additions & 0 deletions spec/app/models/contractor.rb
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,7 @@
class Contractor
include Mongoid::Document
attr_accessible
attr_accessible :name, as: :admin
embedded_in :building
field :name, type: String
end
Loading

0 comments on commit 71fe904

Please sign in to comment.