Created
February 17, 2014 15:35
-
-
Save Nerdman4U/9052758 to your computer and use it in GitHub Desktop.
Autosave, inverse and nested record stack level too deep problem fix
This file contains 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 characters
module ActiveRecord | |
module AutosaveAssociation | |
# Structure is a nested hash containing symbol keys as a valid | |
# reflection names, use only hash values. If you use nil value and | |
# nested record class has own structure, that will be used instead. | |
# | |
# ==== Example | |
# | |
# class Foo | |
# def self.structure | |
# { coffee: { honeys: { rock-n-roll: {} }, bees: {} } } | |
# end | |
# end | |
def structure | |
@structure | |
end | |
# Set current structure for this model. This is set by parent | |
# record. | |
def structure= value | |
@structure = value | |
end | |
# Verifies does the currect structure contain this reflection | |
# | |
# ==== Returns | |
# | |
# ::true | |
# if reflection.name is found from structure or if structure is | |
# omitted. | |
# | |
# ::false | |
# if structure is found but reflection.name is not found from it. | |
# | |
def reflection_in_structure? reflection | |
return true unless structure | |
structure.include?(reflection.name.to_sym) | |
end | |
# Validation of nested records is skipped if either of following is true, | |
# | |
# a) Skip option used at parent (this) record | |
# | |
# >> foo.save(validate: false) | |
# => skip validation, skip nested validation, | |
# do not skip other callbacks. | |
# | |
# b) Structure is in use but does not contain reflection name | |
# | |
# Example: | |
# >> foo.valid? structure: :cool_stuff | |
# => only reflection.targets of "cool stuff" will be validated. | |
# | |
def skip_association_validation? reflection | |
return true if skip_validations? | |
return true unless reflection_in_structure?(reflection) | |
false | |
end | |
# Are we done? | |
def validated_for_autosave? | |
@validated_for_autosave | |
end | |
# Mark this record valid so that following possible nested inverse | |
# association validation will not try to validate this ( and all | |
# reflections ) again. | |
def validated_for_autosave! | |
@validated_for_autosave = true | |
end | |
private | |
# Nested record validation will cause stack level too deep error if | |
# association has records which are inverse associtions to current | |
# record. Prevent this by setting a flag "validated_for_autosave" | |
# which is used to skip a record from inverse | |
# nested_records_changed_for_autosave? call. | |
# | |
# ==== Example | |
# | |
# class Foo | |
# has_many :bars, inverse_of: :foo | |
# accepts_nested_attributes_for :bars | |
# | |
# class Bar | |
# belongs_to :foo, inverse_of: bars | |
# accepts_nested_attributes_for :foo | |
def nested_records_changed_for_autosave? | |
validated_for_autosave! | |
self.class.reflect_on_all_autosave_associations.any? do |reflection| | |
association = association_instance_get(reflection.name) | |
next unless association | |
Array.wrap(association.target).any? do |object| | |
next if object.validated_for_autosave? | |
object.changed_for_autosave? | |
end | |
end | |
end | |
# Just simplified code here, nothing new | |
def records_for_validate_collection reflection | |
association = association_instance_get(reflection.name) | |
return [] unless association.present? | |
associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) | |
end | |
def validate_collection_association(reflection) | |
return if skip_association_validation? reflection | |
records_for_validate_collection(reflection).each do |record| | |
association_valid?(reflection, record) | |
end | |
end | |
alias :original_validate_single_association :validate_single_association | |
def validate_single_association reflection | |
return if skip_association_validation? reflection | |
original_validate_single_association(reflection) | |
end | |
alias :original_association_valid? :association_valid? | |
def association_valid?(reflection, record) | |
if structure and reflection_in_structure?(reflection) and !record.destroyed? | |
record.structure = structure[reflection.name.to_sym] | |
end | |
original_association_valid?(reflection, record) | |
end | |
alias :original_save_collection_association :save_collection_association | |
def save_collection_association(reflection) | |
return unless reflection_in_structure?(reflection) | |
original_save_collection_association(reflection) | |
end | |
alias :original_save_has_one_association :save_has_one_association | |
def save_has_one_association(reflection) | |
return unless reflection_in_structure?(reflection) | |
original_save_has_one_association(reflection) | |
end | |
alias :original_save_belongs_to_association :save_belongs_to_association | |
def save_belongs_to_association(reflection) | |
return unless reflection_in_structure?(reflection) | |
original_save_belongs_to_association(reflection) | |
end | |
end | |
end |
This file contains 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 characters
module ActiveRecord | |
# Use structures to prevent stack level too deep problems with | |
# autosave and nested records. | |
# | |
# ==== Usage | |
# | |
# Type class method "structure" to your class, let it return a hash | |
# of symbols. Similar as includes, joins etc. | |
# | |
# Objects of that class are now validated and saved based on structure | |
# depth. | |
# | |
# You can use structures explicitely by | |
# >> foo.save( structure: { bar:{doh:{}}} ) | |
# | |
# Use only hash values | |
module Validations | |
# Save overwritten due to following problems, | |
# | |
# 1) { validate: false } option will skip all callbacks | |
# | |
# 2) { validate: false } option did not affect to nested autosave | |
# records, they are validated even option is set | |
# | |
# 3) structure is set here, if any | |
# | |
def save(options={}) | |
skip_validations! if options[:validate] == false | |
set_structure! options | |
super if perform_validations(options) | |
end | |
def save!(options={}) | |
skip_validations! if options[:validate] == false | |
set_structure! options | |
perform_validations(options) ? super : raise( RecordInvalid.new(self) ) | |
end | |
def valid?( options=nil ) | |
context = options unless options.is_a? Hash | |
context ||= (new_record? ? :create : :update) | |
options ||= {} | |
set_structure! options | |
output = super(context) | |
errors.empty? && output | |
end | |
private | |
# Set current structure to this record. This is given recursively | |
# from parent record. | |
# | |
# If options[:structure] exists, use it. | |
# If omitted but class has the default structure use class.structure. | |
# If omitted do not use structure. | |
def set_structure! options | |
@structure = options[:structure] | |
@structure = self.class.structure if !@structure and self.class.respond_to? :structure | |
end | |
def skip_validations? | |
!!@skip_validations | |
end | |
def skip_validations! | |
@skip_validations ||= true | |
end | |
protected | |
# Treat record as a valid if validations are skipped. | |
def perform_validations(options={}) | |
return true if skip_validations? | |
valid?(options[:context]) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment