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
Expand Up @@ -7,6 +7,9 @@ For instructions on upgrading to newer versions, visit

### 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.
(Tim Olsen)

Expand Down
69 changes: 58 additions & 11 deletions lib/mongoid/attributes/processing.rb
Expand Up @@ -19,19 +19,41 @@ module Processing
#
# @since 2.0.0.rc.7
def process_attributes(attrs = nil, role = :default, guard_protected_attributes = true)
attrs ||= {}
if !attrs.empty?
attrs = sanitize_for_mass_assignment(attrs, role) if guard_protected_attributes
attrs.each_pair do |key, value|
next if pending_attribute?(key, value)
process_attribute(key, value)
with_mass_assignment(role, guard_protected_attributes) do
attrs ||= {}
if !attrs.empty?
attrs = sanitize_for_mass_assignment(attrs, role) if guard_protected_attributes
attrs.each_pair do |key, value|
next if pending_attribute?(key, value)
process_attribute(key, value)
end
end
yield self if block_given?
process_pending
end
yield self if block_given?
process_pending
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
# 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.
# document.process_pending
#
# @param [ Hash ] options The mass assignment options.
#
# @since 2.0.0.rc.7
def process_pending
process_nested and process_relations
Expand All @@ -133,18 +157,41 @@ def process_pending
# @example Process the relations.
# document.process_relations
#
# @param [ Hash ] options The mass assignment options.
#
# @since 2.0.0.rc.7
def process_relations
pending_relations.each_pair do |name, value|
metadata = relations[name]
if value.is_a?(Hash)
metadata.nested_builder(value, {}).build(self)
metadata.nested_builder(value, {}).build(self, mass_assignment_options)
else
send("#{name}=", value)
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

2 changes: 1 addition & 1 deletion lib/mongoid/factory.rb
Expand Up @@ -12,7 +12,7 @@ module Factory
#
# @param [ Class ] klass The class to instantiate from if _type is not present.
# @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.
def build(klass, attributes = nil, options = {})
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/nested_attributes.rb
Expand Up @@ -52,7 +52,7 @@ def accepts_nested_attributes_for(*args)
autosave(metadata.merge!(autosave: true))
re_define_method(meth) do |attrs|
_assigning do
metadata.nested_builder(attrs, options).build(self)
metadata.nested_builder(attrs, options).build(self, mass_assignment_options)
end
end
end
Expand Down
23 changes: 17 additions & 6 deletions lib/mongoid/relations/builders/nested_attributes/many.rb
Expand Up @@ -16,18 +16,19 @@ class Many < NestedBuilder
# many.build(person)
#
# @param [ Document ] parent The parent document of the relation.
# @param [ Hash ] options The mass assignment options.
#
# @return [ Array ] The attributes.
def build(parent)
def build(parent, options = {})
@existing = parent.send(metadata.name)
if over_limit?(attributes)
raise Errors::TooManyNestedAttributeRecords.new(existing, options[:limit])
end
attributes.each do |attrs|
if attrs.respond_to?(:with_indifferent_access)
process_attributes(parent, attrs)
process_attributes(parent, attrs, options)
else
process_attributes(parent, attrs[1])
process_attributes(parent, attrs[1], options)
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
# new, existing, or ignored document.
#
# @api private
#
# @example Process the attributes
# builder.process_attributes({ "id" => 1, "street" => "Bond" })
#
# @param [ Document ] parent The parent document.
# @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)
if id = attrs.extract_id
first = existing.first
Expand All @@ -100,10 +107,14 @@ def process_attributes(parent, attrs)
doc.destroy unless doc.embedded? || doc.destroyed?
else
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
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
Expand Down
91 changes: 41 additions & 50 deletions lib/mongoid/relations/builders/nested_attributes/one.rb
Expand Up @@ -10,25 +10,27 @@ class One < NestedBuilder
# Builds the relation depending on the attributes and the options
# passed to the macro.
#
# 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.
# @example Build a 1-1 nested document.
# one.build(person, as: :admin)
#
# 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.
def build(parent)
# @since 2.0.0
def build(parent, options = {})
return if reject?(parent, attributes)
@existing = parent.send(metadata.name)
if update?
attributes.delete_id
existing.attributes = attributes
existing.assign_attributes(attributes, options)
elsif replace?
parent.send(metadata.setter, Factory.build(metadata.klass, attributes))
parent.send(metadata.setter, Factory.build(metadata.klass, attributes, options))
elsif delete?
parent.send(metadata.setter, nil)
end
Expand All @@ -37,19 +39,14 @@ def build(parent)
# Create the new builder for nested attributes on one-to-one
# 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:
#
# metadata: The relation metadata
# attributes: The attributes hash to attempt to set.
# options: The options defined.
#
# Returns:
#
# A new builder.
# @since 2.0.0
def initialize(metadata, attributes, options)
@attributes = attributes.with_indifferent_access
@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
# 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?
id = convert_id(existing.class, attributes[:id])
existing.id == id || id.nil? || (existing.id != id && update_only?)
end

# 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:
#
# True if the relation should be deleted.
# @since 2.0.0
def delete?
destroyable? && !attributes[:id].nil?
end

# Can the existing relation potentially be deleted?
#
# Example:
# Can the existing relation potentially be destroyed?
#
# <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.
#
# Returns:
#
# True if the relation can potentially be deleted.
# @since 2.0.0
def destroyable?
[ 1, "1", true, "true" ].include?(destroy) && allow_destroy?
end

# 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:
#
# True if the document should be replaced.
# @since 2.0.0
def replace?
!existing && !destroyable? && !attributes.blank?
end

# Should the document be updated?
#
# Example:
#
# <tt>update?</tt>
# @example Should the document be updated?
# one.update?
#
# 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?
existing && !destroyable? && acceptable_id?
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/relations/embedded/batchable.rb
Expand Up @@ -194,7 +194,7 @@ def normalize_docs(docs)
docs.map do |doc|
attributes = { metadata: metadata, _parent: base }
attributes.merge!(doc)
Factory.build(klass, attributes)
Factory.build(klass, attributes, base.send(:mass_assignment_options))
end
else
docs
Expand Down
7 changes: 7 additions & 0 deletions spec/app/models/building.rb
@@ -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
@@ -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
@@ -0,0 +1,7 @@
class Contractor
include Mongoid::Document
attr_accessible
attr_accessible :name, as: :admin
embedded_in :building
field :name, type: String
end

0 comments on commit 71fe904

Please sign in to comment.