Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Propagate mass assignment options down to children

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...
commit 71fe9042fdb423b4d6253e0bd55caafa2a0e5591 1 parent 7582271
@durran durran authored
View
3  CHANGELOG.md
@@ -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)
View
69 lib/mongoid/attributes/processing.rb
@@ -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
@@ -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
@@ -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
-
View
2  lib/mongoid/factory.rb
@@ -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 = {})
View
2  lib/mongoid/nested_attributes.rb
@@ -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
View
23 lib/mongoid/relations/builders/nested_attributes/many.rb
@@ -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
@@ -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
@@ -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
View
91 lib/mongoid/relations/builders/nested_attributes/one.rb
@@ -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
@@ -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
@@ -62,13 +59,14 @@ 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?)
@@ -76,56 +74,49 @@ def acceptable_id?
# 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
View
2  lib/mongoid/relations/embedded/batchable.rb
@@ -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
View
7 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
View
7 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
View
7 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
View
123 spec/mongoid/attributes/processing_spec.rb
@@ -0,0 +1,123 @@
+require "spec_helper"
+
+describe Mongoid::Attributes::Processing do
+
+ describe "#process_attributes" do
+
+ let(:building) do
+ Building.new
+ end
+
+ context "when setting embedded documents via the parent" do
+
+ let(:attributes) do
+ {
+ building_address: { city: "Berlin" },
+ contractors: [{ name: "Joe" }]
+ }
+ end
+
+ context "when providing a role" do
+
+ context "when creating new documents" do
+
+ before do
+ building.process_attributes(attributes, :admin, true)
+ end
+
+ let(:building_address) do
+ building.building_address
+ end
+
+ let(:contractor) do
+ building.contractors.first
+ end
+
+ context "when the child fields are accessible to the role" do
+
+ it "sets the fields on the 1-1 child" do
+ building_address.city.should eq("Berlin")
+ end
+
+ it "sets the fields on the 1-n child" do
+ contractor.name.should eq("Joe")
+ end
+ end
+
+ context "when updating the document" do
+
+ let(:updates) do
+ {
+ building_address: { city: "Kiew" },
+ contractors: [{ name: "Jim" }]
+ }
+ end
+
+ before do
+ building.process_attributes(updates, :admin, true)
+ end
+
+ it "updates the 1-1 child" do
+ building_address.city.should eq("Kiew")
+ end
+
+ it "updates the 1-n child" do
+ contractor.name.should eq("Jim")
+ end
+ end
+ end
+ end
+
+ context "when turning off mass assignment" do
+
+ context "when creating new documents" do
+
+ before do
+ building.process_attributes(attributes, :default, false)
+ end
+
+ let(:building_address) do
+ building.building_address
+ end
+
+ let(:contractor) do
+ building.contractors.first
+ end
+
+ context "when the child fields are accessible to the role" do
+
+ it "sets the fields on the 1-1 child" do
+ building_address.city.should eq("Berlin")
+ end
+
+ it "sets the fields on the 1-n child" do
+ contractor.name.should eq("Joe")
+ end
+ end
+
+ context "when updating the document" do
+
+ let(:updates) do
+ {
+ building_address: { city: "Kiew" },
+ contractors: [{ name: "Jim" }]
+ }
+ end
+
+ before do
+ building.process_attributes(updates, :default, false)
+ end
+
+ it "updates the 1-1 child" do
+ building_address.city.should eq("Kiew")
+ end
+
+ it "updates the 1-n child" do
+ contractor.name.should eq("Jim")
+ end
+ end
+ end
+ end
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.