# HG changeset patch # User Tyler Rick # Date 1267121472 28800 # Node ID a0ab430243c79b1311925cef5745c2becdbdfb03 # Parent c7287bf557dd8af22fd417f13d894fcb511c8f0c Moved all files up to the root of the repository diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 Rakefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Rakefile Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the has_and_belongs_to_many_with_deferred_save plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the has_and_belongs_to_many_with_deferred_save plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'HasAndBelongsToManyWithDeferredSave' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 Readme --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Readme Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,37 @@ +=has_and_belongs_to_many_with_deferred_save + +==Example + +Room has_and_belongs_to_many_with_deferred_save People + +Lets say you want to validate the room.people collection and prevent the user from adding more people to the room than will fit. If they do try to add more people than will fit, you want to display a nice error message on the page and let them try again... + +This isn't possible using the standard has_and_belongs_to_many due to these two problems: + +1. When I do the assignment to my collection (room.people = whatever), it immediately saves it in my join table (people_rooms) rather than waiting until I call room.save. + +2. You can "validate" using habtm's :before_add option ... but it any errors added there end up being ignored/lost. The only way to abort the save from a before_add seems to be to raise an exception... + +We don't want to raise an exception when the user violates my validation. We want validation of the people collection to be handled the same as any other field in the Room model: We want it to simply add an error to the Room model's error array which we can than display on the form with the other input errors. + +has_and_belongs_to_many_with_deferred_save solves this problem by overriding the setter method for your collection (people=), causing it to store the new members in a temporary variable (unsaved_people rather than saving it immediately. + +You can then validate the unsaved collection as you would any other attribute, adding to self.errors if something is invalid about the collection (too many members, etc.). + +The unsaved collection is automatically saved when you call save on the model. + +==How to install + +./script/plugin install http://habtm-with-deferred-save.googlecode.com/svn/trunk/has_and_belongs_to_many_with_deferred_save + +==Project home + +http://code.google.com/p/habtm-with-deferred-save/ + +==History + +It started as a post to the Rails mailing list asking how to validate a has_and_belongs_to_many collection/association (see http://www.ruby-forum.com/topic/81095). + +==Copyright + +2007 (c) QualitySmith, Inc. diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/Rakefile --- a/has_and_belongs_to_many_with_deferred_save/Rakefile Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,22 +0,0 @@ -require 'rake' -require 'rake/testtask' -require 'rake/rdoctask' - -desc 'Default: run unit tests.' -task :default => :test - -desc 'Test the has_and_belongs_to_many_with_deferred_save plugin.' -Rake::TestTask.new(:test) do |t| - t.libs << 'lib' - t.pattern = 'test/**/*_test.rb' - t.verbose = true -end - -desc 'Generate documentation for the has_and_belongs_to_many_with_deferred_save plugin.' -Rake::RDocTask.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'HasAndBelongsToManyWithDeferredSave' - rdoc.options << '--line-numbers' << '--inline-source' - rdoc.rdoc_files.include('README') - rdoc.rdoc_files.include('lib/**/*.rb') -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/Readme --- a/has_and_belongs_to_many_with_deferred_save/Readme Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,37 +0,0 @@ -=has_and_belongs_to_many_with_deferred_save - -==Example - -Room has_and_belongs_to_many_with_deferred_save People - -Lets say you want to validate the room.people collection and prevent the user from adding more people to the room than will fit. If they do try to add more people than will fit, you want to display a nice error message on the page and let them try again... - -This isn't possible using the standard has_and_belongs_to_many due to these two problems: - -1. When I do the assignment to my collection (room.people = whatever), it immediately saves it in my join table (people_rooms) rather than waiting until I call room.save. - -2. You can "validate" using habtm's :before_add option ... but it any errors added there end up being ignored/lost. The only way to abort the save from a before_add seems to be to raise an exception... - -We don't want to raise an exception when the user violates my validation. We want validation of the people collection to be handled the same as any other field in the Room model: We want it to simply add an error to the Room model's error array which we can than display on the form with the other input errors. - -has_and_belongs_to_many_with_deferred_save solves this problem by overriding the setter method for your collection (people=), causing it to store the new members in a temporary variable (unsaved_people rather than saving it immediately. - -You can then validate the unsaved collection as you would any other attribute, adding to self.errors if something is invalid about the collection (too many members, etc.). - -The unsaved collection is automatically saved when you call save on the model. - -==How to install - -./script/plugin install http://habtm-with-deferred-save.googlecode.com/svn/trunk/has_and_belongs_to_many_with_deferred_save - -==Project home - -http://code.google.com/p/habtm-with-deferred-save/ - -==History - -It started as a post to the Rails mailing list asking how to validate a has_and_belongs_to_many collection/association (see http://www.ruby-forum.com/topic/81095). - -==Copyright - -2007 (c) QualitySmith, Inc. diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/init.rb --- a/has_and_belongs_to_many_with_deferred_save/init.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -require File.join(File.dirname(__FILE__), 'lib/has_and_belongs_to_many_with_deferred_save') diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/install.rb --- a/has_and_belongs_to_many_with_deferred_save/install.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -# Install hook code here diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/lib/has_and_belongs_to_many_with_deferred_save.rb --- a/has_and_belongs_to_many_with_deferred_save/lib/has_and_belongs_to_many_with_deferred_save.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,133 +0,0 @@ -# To do: make it work to call this twice in a class. Currently that probably wouldn't work, because it would try to alias methods to existing names... -# Note: before_save must be defined *before* including this module, not after. - -module ActiveRecord - module Associations - module ClassMethods - # Instructions: - # - # Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save. - # - # Then add a validation method that adds an error if there is something wrong with the (unsaved) collection. This will prevent it from being saved if there are any errors. - # - # Example: - # - # def validate - # if people.size > maximum_occupancy - # errors.add :people, "There are too many people in this room" - # end - # end - def has_and_belongs_to_many_with_deferred_save(*args) - has_and_belongs_to_many *args - collection_name = args[0].to_s - collection_singular_ids = collection_name.singularize + "_ids" - - # this will delete all the assocation into the join table after obj.destroy - after_destroy { |record| record.save } - - attr_accessor :"unsaved_#{collection_name}" - attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}" - - define_method "#{collection_name}_with_deferred_save=" do |collection| - #puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}" - self.send "unsaved_#{collection_name}=", collection - end - - define_method "#{collection_name}_with_deferred_save" do |*args| - if self.send("use_original_collection_reader_behavior_for_#{collection_name}") - self.send("#{collection_name}_without_deferred_save") - else - if self.send("unsaved_#{collection_name}").nil? - send("initialize_unsaved_#{collection_name}", *args) - end - self.send("unsaved_#{collection_name}") - end - end - - alias_method_chain :"#{collection_name}=", 'deferred_save' - alias_method_chain :"#{collection_name}", 'deferred_save' - - define_method "#{collection_singular_ids}_with_deferred_save" do |*args| - if self.send("use_original_collection_reader_behavior_for_#{collection_name}") - self.send("#{collection_singular_ids}_without_deferred_save") - else - if self.send("unsaved_#{collection_name}").nil? - send("initialize_unsaved_#{collection_name}", *args) - end - self.send("unsaved_#{collection_name}").map { |e| e[:id] } - end - end - - alias_method_chain :"#{collection_singular_ids}", 'deferred_save' - - - define_method "before_save_with_deferred_save_for_#{collection_name}" do - # Question: Why do we need this @use_original_collection_reader_behavior stuff? - # Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only - # records that have changed. - # In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not - # knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database* - # (the original behavior), so we have to provide that behavior... If we didn't provide it, it would end up trying to take the diff of - # two identical collections so nothing would ever get saved. - # But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use - # @use_original_collection_reader_behavior as a switch. - - if self.respond_to? :"before_save_without_deferred_save_for_#{collection_name}" - self.send("before_save_without_deferred_save_for_#{collection_name}") - end - - self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true - if self.send("unsaved_#{collection_name}").nil? - send("initialize_unsaved_#{collection_name}", *args) - end - self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}") - # /\ This is where the actual save occurs. - self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false - - true - end - alias_method_chain :"before_save", "deferred_save_for_#{collection_name}" - - - define_method "reload_with_deferred_save_for_#{collection_name}" do - # Reload from the *database*, discarding any unsaved changes. - returning self.send("reload_without_deferred_save_for_#{collection_name}") do - self.send "unsaved_#{collection_name}=", nil - # /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of - # unsaved_collection that it had before the reload. - end - end - alias_method_chain :"reload", "deferred_save_for_#{collection_name}" - - - define_method "initialize_unsaved_#{collection_name}" do |*args| - #puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}" - self.send "unsaved_#{collection_name}=", self.send("#{collection_name}_without_deferred_save", *args).clone - # /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the - # database, in which case we want unsaved_collection to start out with the "saved collection". - # If they just constructed a *new* object, this will still work, because self.collection_without_deferred_save.clone - # will return a new HasAndBelongsToManyAssociation (which acts like an empty array, []). - # Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection - # will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things - # immediately, which is exactly what we're trying to avoid.) - - # trick collection_name.include?(obj) - # If you use a collection of SignelTableInheritance and didn't :select 'type' the - # include? method will not find any subclassed object. - class << eval("@unsaved_#{collection_name}") - def include_with_deferred_save?(obj) - if self.find { |itm| itm == obj || (itm[:id] == obj[:id] && obj.is_a?(itm.class) ) } - return true - else - return false - end - end - alias_method_chain :include?, 'deferred_save' - end - end - private :"initialize_unsaved_#{collection_name}" - - end - end - end -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/Rakefile --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/Rakefile Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,10 +0,0 @@ -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require(File.join(File.dirname(__FILE__), 'config', 'boot')) - -require 'rake' -require 'rake/testtask' -require 'rake/rdoctask' - -require 'tasks/rails' diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/app/controllers/application.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/app/controllers/application.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -# Filters added to this controller apply to all controllers in the application. -# Likewise, all the methods added will be available for all controllers. - -class ApplicationController < ActionController::Base - # Pick a unique cookie name to distinguish our session data from others' - session :session_key => '_rails_root_session_id' -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/app/helpers/application_helper.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/app/helpers/application_helper.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -# Methods added to this helper will be available to all templates in the application. -module ApplicationHelper -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/app/models/person.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/app/models/person.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -class Person < ActiveRecord::Base - has_and_belongs_to_many :room -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/app/models/room.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/app/models/room.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,17 +0,0 @@ -class Room < ActiveRecord::Base - has_and_belongs_to_many_with_deferred_save :people, :before_add => :before_adding_person - - def validate - if people.size > maximum_occupancy - errors.add :people, "There are too many people in this room" - end - end - - # Just in case they try to bypass our new accessor and call people_without_deferred_save directly... - # (This should never be necessary; it is for demonstration purposes only...) - def before_adding_person(person) - if self.people_without_deferred_save.size + [person].size > maximum_occupancy - raise "There are too many people in this room" - end - end -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/config/boot.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/config/boot.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,45 +0,0 @@ -# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb - -unless defined?(RAILS_ROOT) - root_path = File.join(File.dirname(__FILE__), '..') - - unless RUBY_PLATFORM =~ /(:?mswin|mingw)/ - require 'pathname' - root_path = Pathname.new(root_path).cleanpath(true).to_s - end - - RAILS_ROOT = root_path -end - -unless defined?(Rails::Initializer) - if File.directory?("#{RAILS_ROOT}/vendor/rails") - require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" - else - require 'rubygems' - - environment_without_comments = IO.readlines(File.dirname(__FILE__) + '/environment.rb').reject { |l| l =~ /^#/ }.join - environment_without_comments =~ /[^#]RAILS_GEM_VERSION = '([\d.]+)'/ - rails_gem_version = $1 - - if version = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION : rails_gem_version - # Asking for 1.1.6 will give you 1.1.6.5206, if available -- makes it easier to use beta gems - rails_gem = Gem.cache.search('rails', "~>#{version}.0").sort_by { |g| g.version.version }.last - - if rails_gem - gem "rails", "=#{rails_gem.version.version}" - require rails_gem.full_gem_path + '/lib/initializer' - else - STDERR.puts %(Cannot find gem for Rails ~>#{version}.0: - Install the missing gem with 'gem install -v=#{version} rails', or - change environment.rb to define RAILS_GEM_VERSION with your desired version. - ) - exit 1 - end - else - gem "rails" - require 'initializer' - end - end - - Rails::Initializer.run(:set_load_path) -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/config/database.yml --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/config/database.yml Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -development: - adapter: sqlite3 - database: db/development.sqlite - -test: - adapter: sqlite3 - database: db/test.sqlite - -production: - adapter: sqlite3 - database: db/production.sqlite diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/config/environment.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/config/environment.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,60 +0,0 @@ -# Be sure to restart your web server when you modify this file. - -# Uncomment below to force Rails into production mode when -# you don't control web/app server and can't set it the proper way -# ENV['RAILS_ENV'] ||= 'production' - -# Specifies gem version of Rails to use when vendor/rails is not present -RAILS_GEM_VERSION = '1.2.3' unless defined? RAILS_GEM_VERSION - -# Bootstrap the Rails environment, frameworks, and default configuration -require File.join(File.dirname(__FILE__), 'boot') - -Rails::Initializer.run do |config| - # Settings in config/environments/* take precedence over those specified here - - # Skip frameworks you're not going to use (only works if using vendor/rails) - # config.frameworks -= [ :action_web_service, :action_mailer ] - - # Only load the plugins named here, by default all plugins in vendor/plugins are loaded - # config.plugins = %W( exception_notification ssl_requirement ) - - # Add additional load paths for your own custom dirs - # config.load_paths += %W( #{RAILS_ROOT}/extras ) - - # Force all environments to use the same logger level - # (by default production uses :info, the others :debug) - # config.log_level = :debug - - # Use the database for sessions instead of the file system - # (create the session table with 'rake db:sessions:create') - # config.action_controller.session_store = :active_record_store - - # Use SQL instead of Active Record's schema dumper when creating the test database. - # This is necessary if your schema can't be completely dumped by the schema dumper, - # like if you have constraints or database-specific column types - # config.active_record.schema_format = :sql - - # Activate observers that should always be running - # config.active_record.observers = :cacher, :garbage_collector - - # Make Active Record use UTC-base instead of local time - # config.active_record.default_timezone = :utc - - # See Rails::Configuration for more options -end - -# Add new inflection rules using the following format -# (all these examples are active by default): -# Inflector.inflections do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' -# inflect.uncountable %w( fish sheep ) -# end - -# Add new mime types for use in respond_to blocks: -# Mime::Type.register "text/richtext", :rtf -# Mime::Type.register "application/x-mobile", :mobile - -# Include your application configuration below \ No newline at end of file diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/config/environments/development.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/config/environments/development.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# Settings specified here will take precedence over those in config/environment.rb - -# In the development environment your application's code is reloaded on -# every request. This slows down response time but is perfect for development -# since you don't have to restart the webserver when you make code changes. -config.cache_classes = false - -# Log error messages when you accidentally call methods on nil. -config.whiny_nils = true - -# Enable the breakpoint server that script/breakpointer connects to -config.breakpoint_server = true - -# Show full error reports and disable caching -config.action_controller.consider_all_requests_local = true -config.action_controller.perform_caching = false -config.action_view.cache_template_extensions = false -config.action_view.debug_rjs = true - -# Don't care if the mailer can't send -config.action_mailer.raise_delivery_errors = false diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/config/environments/production.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/config/environments/production.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,18 +0,0 @@ -# Settings specified here will take precedence over those in config/environment.rb - -# The production environment is meant for finished, "live" apps. -# Code is not reloaded between requests -config.cache_classes = true - -# Use a different logger for distributed setups -# config.logger = SyslogLogger.new - -# Full error reports are disabled and caching is turned on -config.action_controller.consider_all_requests_local = false -config.action_controller.perform_caching = true - -# Enable serving of images, stylesheets, and javascripts from an asset server -# config.action_controller.asset_host = "http://assets.example.com" - -# Disable delivery errors, bad email addresses will be ignored -# config.action_mailer.raise_delivery_errors = false diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/config/environments/test.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/config/environments/test.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -# Settings specified here will take precedence over those in config/environment.rb - -# The test environment is used exclusively to run your application's -# test suite. You never need to work with it otherwise. Remember that -# your test database is "scratch space" for the test suite and is wiped -# and recreated between test runs. Don't rely on the data there! -config.cache_classes = true - -# Log error messages when you accidentally call methods on nil. -config.whiny_nils = true - -# Show full error reports and disable caching -config.action_controller.consider_all_requests_local = true -config.action_controller.perform_caching = false - -# Tell ActionMailer not to deliver emails to the real world. -# The :test delivery method accumulates sent emails in the -# ActionMailer::Base.deliveries array. -config.action_mailer.delivery_method = :test \ No newline at end of file diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/config/routes.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/config/routes.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,23 +0,0 @@ -ActionController::Routing::Routes.draw do |map| - # The priority is based upon order of creation: first created -> highest priority. - - # Sample of regular route: - # map.connect 'products/:id', :controller => 'catalog', :action => 'view' - # Keep in mind you can assign values other than :controller and :action - - # Sample of named route: - # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase' - # This route can be invoked with purchase_url(:id => product.id) - - # You can have the root of your site routed by hooking up '' - # -- just remember to delete public/index.html. - # map.connect '', :controller => "welcome" - - # Allow downloading Web Service WSDL as a file with an extension - # instead of a file named 'wsdl' - map.connect ':controller/service.wsdl', :action => 'wsdl' - - # Install the default route as the lowest priority. - map.connect ':controller/:action/:id.:format' - map.connect ':controller/:action/:id' -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/db/migrate/001_create_rooms_and_people.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/db/migrate/001_create_rooms_and_people.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -class CreateRoomsAndPeople < ActiveRecord::Migration - def self.up - create_table :people do |t| - t.column :name, :string - end - create_table :rooms do |t| - t.column :name, :string - t.column :maximum_occupancy, :integer - end - create_table :people_rooms do |t| - t.column :person_id, :integer - t.column :room_id, :integer - end - end - - def self.down - drop_table :people - drop_table :rooms - drop_table :people_rooms - end -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/db/schema.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/db/schema.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# This file is autogenerated. Instead of editing this file, please use the -# migrations feature of ActiveRecord to incrementally modify your database, and -# then regenerate this schema definition. - -ActiveRecord::Schema.define(:version => 1) do - - create_table "people", :force => true do |t| - t.column "name", :string - end - - create_table "people_rooms", :force => true do |t| - t.column "person_id", :integer - t.column "room_id", :integer - end - - create_table "rooms", :force => true do |t| - t.column "name", :string - t.column "maximum_occupancy", :integer - end - -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/log/development.log --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/log/development.log Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,14 +0,0 @@ - SQL (0.000761) SELECT * FROM schema_info - SQL (0.000436) SELECT name FROM sqlite_master WHERE type = 'table' - SQL (0.000085) PRAGMA index_list(people) - SQL (0.000082) PRAGMA index_list(people_rooms) - SQL (0.000083) PRAGMA index_list(rooms) - SQL (0.000000) SQLite3::SQLException: no such table: people: DROP TABLE people - SQL (0.029406) CREATE TABLE people ("id" INTEGER PRIMARY KEY NOT NULL, "name" varchar(255) DEFAULT NULL)  - SQL (0.000000) SQLite3::SQLException: no such table: people_rooms: DROP TABLE people_rooms - SQL (0.002897) CREATE TABLE people_rooms ("id" INTEGER PRIMARY KEY NOT NULL, "person_id" integer DEFAULT NULL, "room_id" integer DEFAULT NULL)  - SQL (0.000000) SQLite3::SQLException: no such table: rooms: DROP TABLE rooms - SQL (0.002889) CREATE TABLE rooms ("id" INTEGER PRIMARY KEY NOT NULL, "name" varchar(255) DEFAULT NULL, "maximum_occupancy" integer DEFAULT NULL)  - SQL (0.003147) CREATE TABLE schema_info (version integer) - SQL (0.004004) INSERT INTO schema_info (version) VALUES(0) - SQL (0.002317) UPDATE schema_info SET version = 1 diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/log/test.log --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/log/test.log Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ - Person Load (0.000348) SELECT * FROM people WHERE (people."id" = 1)  - Person Load (0.000261) SELECT * FROM people WHERE (people."id" = 2)  - Room Count (0.000260) select count(*) from people_rooms - SQL (0.000425) INSERT INTO rooms ("name", "maximum_occupancy") VALUES(NULL, 2) - SQL (0.000256) INSERT INTO people_rooms ("id", "room_id", "person_id") VALUES (1, 1, 1) - SQL (0.000152) INSERT INTO people_rooms ("id", "room_id", "person_id") VALUES (2, 1, 2) - Room Count (0.000213) select count(*) from people_rooms - Person Load (0.000224) SELECT * FROM people WHERE (people."id" = 3)  - Room Count (0.000183) select count(*) from people_rooms - Room Count (0.000185) select count(*) from people_rooms - Room Load (0.000237) SELECT * FROM rooms WHERE (rooms."id" = 1)  - Person Load (0.000560) SELECT * FROM people INNER JOIN people_rooms ON people.id = people_rooms.person_id WHERE (people_rooms.room_id = 1 )  - Room Count (0.000185) select count(*) from people_rooms diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/test/fixtures/people.yml --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/test/fixtures/people.yml Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html -person1: - id: 1 -person2: - id: 2 -person3: - id: 3 diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/test/fixtures/rooms.yml --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/test/fixtures/rooms.yml Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html -first: - id: 1 -another: - id: 2 diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/test/test_helper.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/test/test_helper.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,28 +0,0 @@ -ENV["RAILS_ENV"] = "test" -require File.expand_path(File.dirname(__FILE__) + "/../config/environment") -require 'test_help' - -class Test::Unit::TestCase - # Transactional fixtures accelerate your tests by wrapping each test method - # in a transaction that's rolled back on completion. This ensures that the - # test database remains unchanged so your fixtures don't have to be reloaded - # between every test method. Fewer database queries means faster tests. - # - # Read Mike Clark's excellent walkthrough at - # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting - # - # Every Active Record database supports transactions except MyISAM tables - # in MySQL. Turn off transactional fixtures in this case; however, if you - # don't care one way or the other, switching from MyISAM to InnoDB tables - # is recommended. - self.use_transactional_fixtures = true - - # Instantiated fixtures are slow, but give you @david where otherwise you - # would need people(:david). If you don't want to migrate your existing - # test cases which use the @david style and don't mind the speed hit (each - # instantiated fixtures translates to a database query per test method), - # then set this back to true. - self.use_instantiated_fixtures = false - - # Add more helper methods to be used by all tests here... -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/test/unit/room_maximum_occupancy_test.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/test/unit/room_maximum_occupancy_test.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -require File.dirname(__FILE__) + '/../test_helper' - -class RoomMaximumOccupancyTest < Test::Unit::TestCase - fixtures :people - -# def test_1 -# room = Room.new(:maximum_occupancy => 4) -# room.maximum_occupancy = 10 -# -# assert_equal 10, room.maximum_occupancy # Still invalid -# assert_equal false, room.save -# assert_equal "You can't have the maximum set so high", room.errors.on(:maximum_occupancy) -# assert_equal 10, room.maximum_occupancy # Still invalid -# end - - def test_maximum_occupancy - room = Room.new(:maximum_occupancy => 2) - assert_equal [], room.people - assert_equal [], room.people_without_deferred_save - assert_not_equal room.unsaved_people.object_id, - room.people_without_deferred_save.object_id - - assert_nothing_raised { room.people << people(:person1) } - assert_nothing_raised { room.people << people(:person2) } - assert_equal 0, Room.count_by_sql("select count(*) from people_rooms") # Still not saved to the association table! - assert_equal 0, room.people_without_deferred_save.size - assert_equal 2, room.people.size # 2 because this looks at unsaved_people - - assert room.save # Only here is it actually saved to the association table! - assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") - assert_equal 2, room.people.size - assert_equal 2, room.people_without_deferred_save.size - - assert_nothing_raised { room.people << people(:person3) } - assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") # person3 is not yet saved to the association table - assert_equal false, room.valid? - assert_equal "There are too many people in this room", room.errors.on(:people) - - assert_equal false, room.save - assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") # It's still not there, because it didn't pass the validation. - assert_equal "There are too many people in this room", room.errors.on(:people) - assert_equal 3, room.people.size # Just like with normal attributes that fail validation... the attribute still contains the invalid data but we refuse to save until it is changed to something that is *valid*. - - room.reload - assert_equal 2, room.people.size - assert_equal 2, room.people_without_deferred_save.size - - assert_nothing_raised { room.people << people(:person3) } - assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") # person3 is not yet saved to the association table - - # If they try to go around our accessors and use the original accessors, then (and only then) will the exception be raised in before_adding_person... - assert_raise RuntimeError do - room.people_without_deferred_save << people(:person3) - end - end -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/test/unit/room_maximum_occupancy_test_1.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/test/unit/room_maximum_occupancy_test_1.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,26 +0,0 @@ -require File.dirname(__FILE__) + '/../test_helper' - -class RoomMaximumOccupancyTest < Test::Unit::TestCase - fixtures :people - - def test_maximum_occupancy - room = Room.new(:maximum_occupancy => 2) - assert_equal 0, Room.count_by_sql("select count(*) from people_rooms") - assert_equal 0, room.people.size - - room.people << people(:person1) - room.people << people(:person2) - assert room.save - assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") - assert_equal 2, room.people.size - - room.people << people(:person3) - #assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") # FAILS because it saves it in people_rooms before we even call room.save ! - - assert_equal false, room.save - # Good, it has the error ... - assert_equal "There are too many people in this room", room.errors.on(:people) - # ... but it's too late. It didn't prevent the invalid data from getting in there! - #assert_equal 2, room.people.size # FAILS - end -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/test/unit/room_maximum_occupancy_test_2.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/test/unit/room_maximum_occupancy_test_2.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,31 +0,0 @@ -require File.dirname(__FILE__) + '/../test_helper' - -class RoomMaximumOccupancyTest < Test::Unit::TestCase - fixtures :people - - def test_maximum_occupancy_using_build - room = Room.new(:maximum_occupancy => 2) - assert_equal 0, Room.count_by_sql("select count(*) from people_rooms") - assert_equal 0, room.people.size - - room.people.build(:name => 'person1') - room.people.build(:name => 'person2') - assert room.save - assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") - assert_equal 2, room.people.size - - room.people.build(:name => 'person3') - # Good, it prevented it from being saved to the database ... - assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") - # ... but it still added it to the collection stored in memory! - #assert_equal 2, room.people.size # Still FAILs. It thinks it has 3, even though the 3rd one is invalid. - - assert_equal false, room.save - assert_equal "There are too many people in this room", room.errors.on(:people) - - # If we reload from what is stored in memory, it will still just have the 2 valid people... - room.reload - assert_equal 2, room.people.size - end - -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/test/unit/room_maximum_occupancy_test_3.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/test/unit/room_maximum_occupancy_test_3.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ -require File.dirname(__FILE__) + '/../test_helper' - -class RoomMaximumOccupancyTest < Test::Unit::TestCase - fixtures :people - - def test_maximum_occupancy - room = Room.new(:maximum_occupancy => 2) - assert_equal 0, Room.count_by_sql("select count(*) from people_rooms") - assert_equal 0, room.people.size - - assert_nothing_raised { room.people << people(:person1) } - assert_nothing_raised { room.people << people(:person2) } - assert room.save - assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") - assert_equal 2, room.people.size - - assert_raise RuntimeError do - room.people << people(:person3) - end - assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") - - assert_equal "There are too many people in this room", room.errors.on(:people) # Passes (for now!) - - # But as soon as I go to save it, it clears out the errors array!! Arg! - room.save - #assert_equal "There are too many people in this room", room.errors.on(:people) # FAILS - - #assert_equal false, room.valid? # FAILS - #assert_equal false, room.save # FAILS - assert_equal 2, room.people.size - end - -end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/rails_root/vendor/plugins/has_and_belong_to_many_with_deferred_save/init.rb --- a/has_and_belongs_to_many_with_deferred_save/test/rails_root/vendor/plugins/has_and_belong_to_many_with_deferred_save/init.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -# This tricks the test app into loading the plugin from its parent directory. -# The alternatives migth be: -# * creating a symlink from plugin_name/rails_root/vendor/plugins/plugin_name to plugin_name -# * creating an svn:external at plugin_name/rails_root/vendor/plugins/plugin_name that pulls in the contents of plugin_name (not only would that create a circular dependency, it also would mean that changes you made to plugin_name wouldn't show up in rails_root until you committed and updated) - -#require "#{RAILS_ROOT}/../init.rb" -init_path = "#{RAILS_ROOT}/../../init.rb" -silence_warnings { eval(IO.read(init_path), binding, init_path) } diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/test/test_helper.rb --- a/has_and_belongs_to_many_with_deferred_save/test/test_helper.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ -require 'rubygems' -require 'facets/core/kernel/require_local' - -# This loads the application's (default) test_helper, which loads the environment, etc. -require_local 'rails_root/test/test_helper' - -# This puts our working directory into the root of our test app. -Dir.chdir File.dirname(__FILE__) + 'rails_root/' - -# Side effect you should be aware of: -# rake_test_loader will have trouble finding your tests due to this chdir... - diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 has_and_belongs_to_many_with_deferred_save/uninstall.rb --- a/has_and_belongs_to_many_with_deferred_save/uninstall.rb Sat Feb 13 13:37:06 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -# Uninstall hook code here diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 init.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/init.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,1 @@ +require File.join(File.dirname(__FILE__), 'lib/has_and_belongs_to_many_with_deferred_save') diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 install.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/install.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,1 @@ +# Install hook code here diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 lib/has_and_belongs_to_many_with_deferred_save.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/has_and_belongs_to_many_with_deferred_save.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,133 @@ +# To do: make it work to call this twice in a class. Currently that probably wouldn't work, because it would try to alias methods to existing names... +# Note: before_save must be defined *before* including this module, not after. + +module ActiveRecord + module Associations + module ClassMethods + # Instructions: + # + # Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save. + # + # Then add a validation method that adds an error if there is something wrong with the (unsaved) collection. This will prevent it from being saved if there are any errors. + # + # Example: + # + # def validate + # if people.size > maximum_occupancy + # errors.add :people, "There are too many people in this room" + # end + # end + def has_and_belongs_to_many_with_deferred_save(*args) + has_and_belongs_to_many *args + collection_name = args[0].to_s + collection_singular_ids = collection_name.singularize + "_ids" + + # this will delete all the assocation into the join table after obj.destroy + after_destroy { |record| record.save } + + attr_accessor :"unsaved_#{collection_name}" + attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}" + + define_method "#{collection_name}_with_deferred_save=" do |collection| + #puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}" + self.send "unsaved_#{collection_name}=", collection + end + + define_method "#{collection_name}_with_deferred_save" do |*args| + if self.send("use_original_collection_reader_behavior_for_#{collection_name}") + self.send("#{collection_name}_without_deferred_save") + else + if self.send("unsaved_#{collection_name}").nil? + send("initialize_unsaved_#{collection_name}", *args) + end + self.send("unsaved_#{collection_name}") + end + end + + alias_method_chain :"#{collection_name}=", 'deferred_save' + alias_method_chain :"#{collection_name}", 'deferred_save' + + define_method "#{collection_singular_ids}_with_deferred_save" do |*args| + if self.send("use_original_collection_reader_behavior_for_#{collection_name}") + self.send("#{collection_singular_ids}_without_deferred_save") + else + if self.send("unsaved_#{collection_name}").nil? + send("initialize_unsaved_#{collection_name}", *args) + end + self.send("unsaved_#{collection_name}").map { |e| e[:id] } + end + end + + alias_method_chain :"#{collection_singular_ids}", 'deferred_save' + + + define_method "before_save_with_deferred_save_for_#{collection_name}" do + # Question: Why do we need this @use_original_collection_reader_behavior stuff? + # Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only + # records that have changed. + # In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not + # knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database* + # (the original behavior), so we have to provide that behavior... If we didn't provide it, it would end up trying to take the diff of + # two identical collections so nothing would ever get saved. + # But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use + # @use_original_collection_reader_behavior as a switch. + + if self.respond_to? :"before_save_without_deferred_save_for_#{collection_name}" + self.send("before_save_without_deferred_save_for_#{collection_name}") + end + + self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true + if self.send("unsaved_#{collection_name}").nil? + send("initialize_unsaved_#{collection_name}", *args) + end + self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}") + # /\ This is where the actual save occurs. + self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false + + true + end + alias_method_chain :"before_save", "deferred_save_for_#{collection_name}" + + + define_method "reload_with_deferred_save_for_#{collection_name}" do + # Reload from the *database*, discarding any unsaved changes. + returning self.send("reload_without_deferred_save_for_#{collection_name}") do + self.send "unsaved_#{collection_name}=", nil + # /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of + # unsaved_collection that it had before the reload. + end + end + alias_method_chain :"reload", "deferred_save_for_#{collection_name}" + + + define_method "initialize_unsaved_#{collection_name}" do |*args| + #puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}" + self.send "unsaved_#{collection_name}=", self.send("#{collection_name}_without_deferred_save", *args).clone + # /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the + # database, in which case we want unsaved_collection to start out with the "saved collection". + # If they just constructed a *new* object, this will still work, because self.collection_without_deferred_save.clone + # will return a new HasAndBelongsToManyAssociation (which acts like an empty array, []). + # Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection + # will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things + # immediately, which is exactly what we're trying to avoid.) + + # trick collection_name.include?(obj) + # If you use a collection of SignelTableInheritance and didn't :select 'type' the + # include? method will not find any subclassed object. + class << eval("@unsaved_#{collection_name}") + def include_with_deferred_save?(obj) + if self.find { |itm| itm == obj || (itm[:id] == obj[:id] && obj.is_a?(itm.class) ) } + return true + else + return false + end + end + alias_method_chain :include?, 'deferred_save' + end + end + private :"initialize_unsaved_#{collection_name}" + + end + end + end +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/Rakefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/Rakefile Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,10 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/app/controllers/application.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/app/controllers/application.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,7 @@ +# Filters added to this controller apply to all controllers in the application. +# Likewise, all the methods added will be available for all controllers. + +class ApplicationController < ActionController::Base + # Pick a unique cookie name to distinguish our session data from others' + session :session_key => '_rails_root_session_id' +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/app/helpers/application_helper.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/app/helpers/application_helper.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,3 @@ +# Methods added to this helper will be available to all templates in the application. +module ApplicationHelper +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/app/models/person.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/app/models/person.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,3 @@ +class Person < ActiveRecord::Base + has_and_belongs_to_many :room +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/app/models/room.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/app/models/room.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,17 @@ +class Room < ActiveRecord::Base + has_and_belongs_to_many_with_deferred_save :people, :before_add => :before_adding_person + + def validate + if people.size > maximum_occupancy + errors.add :people, "There are too many people in this room" + end + end + + # Just in case they try to bypass our new accessor and call people_without_deferred_save directly... + # (This should never be necessary; it is for demonstration purposes only...) + def before_adding_person(person) + if self.people_without_deferred_save.size + [person].size > maximum_occupancy + raise "There are too many people in this room" + end + end +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/config/boot.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/config/boot.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,45 @@ +# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb + +unless defined?(RAILS_ROOT) + root_path = File.join(File.dirname(__FILE__), '..') + + unless RUBY_PLATFORM =~ /(:?mswin|mingw)/ + require 'pathname' + root_path = Pathname.new(root_path).cleanpath(true).to_s + end + + RAILS_ROOT = root_path +end + +unless defined?(Rails::Initializer) + if File.directory?("#{RAILS_ROOT}/vendor/rails") + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" + else + require 'rubygems' + + environment_without_comments = IO.readlines(File.dirname(__FILE__) + '/environment.rb').reject { |l| l =~ /^#/ }.join + environment_without_comments =~ /[^#]RAILS_GEM_VERSION = '([\d.]+)'/ + rails_gem_version = $1 + + if version = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION : rails_gem_version + # Asking for 1.1.6 will give you 1.1.6.5206, if available -- makes it easier to use beta gems + rails_gem = Gem.cache.search('rails', "~>#{version}.0").sort_by { |g| g.version.version }.last + + if rails_gem + gem "rails", "=#{rails_gem.version.version}" + require rails_gem.full_gem_path + '/lib/initializer' + else + STDERR.puts %(Cannot find gem for Rails ~>#{version}.0: + Install the missing gem with 'gem install -v=#{version} rails', or + change environment.rb to define RAILS_GEM_VERSION with your desired version. + ) + exit 1 + end + else + gem "rails" + require 'initializer' + end + end + + Rails::Initializer.run(:set_load_path) +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/config/database.yml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/config/database.yml Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,11 @@ +development: + adapter: sqlite3 + database: db/development.sqlite + +test: + adapter: sqlite3 + database: db/test.sqlite + +production: + adapter: sqlite3 + database: db/production.sqlite diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/config/environment.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/config/environment.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,60 @@ +# Be sure to restart your web server when you modify this file. + +# Uncomment below to force Rails into production mode when +# you don't control web/app server and can't set it the proper way +# ENV['RAILS_ENV'] ||= 'production' + +# Specifies gem version of Rails to use when vendor/rails is not present +RAILS_GEM_VERSION = '1.2.3' unless defined? RAILS_GEM_VERSION + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + # Settings in config/environments/* take precedence over those specified here + + # Skip frameworks you're not going to use (only works if using vendor/rails) + # config.frameworks -= [ :action_web_service, :action_mailer ] + + # Only load the plugins named here, by default all plugins in vendor/plugins are loaded + # config.plugins = %W( exception_notification ssl_requirement ) + + # Add additional load paths for your own custom dirs + # config.load_paths += %W( #{RAILS_ROOT}/extras ) + + # Force all environments to use the same logger level + # (by default production uses :info, the others :debug) + # config.log_level = :debug + + # Use the database for sessions instead of the file system + # (create the session table with 'rake db:sessions:create') + # config.action_controller.session_store = :active_record_store + + # Use SQL instead of Active Record's schema dumper when creating the test database. + # This is necessary if your schema can't be completely dumped by the schema dumper, + # like if you have constraints or database-specific column types + # config.active_record.schema_format = :sql + + # Activate observers that should always be running + # config.active_record.observers = :cacher, :garbage_collector + + # Make Active Record use UTC-base instead of local time + # config.active_record.default_timezone = :utc + + # See Rails::Configuration for more options +end + +# Add new inflection rules using the following format +# (all these examples are active by default): +# Inflector.inflections do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf +# Mime::Type.register "application/x-mobile", :mobile + +# Include your application configuration below \ No newline at end of file diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/config/environments/development.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/config/environments/development.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,21 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# In the development environment your application's code is reloaded on +# every request. This slows down response time but is perfect for development +# since you don't have to restart the webserver when you make code changes. +config.cache_classes = false + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Enable the breakpoint server that script/breakpointer connects to +config.breakpoint_server = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false +config.action_view.cache_template_extensions = false +config.action_view.debug_rjs = true + +# Don't care if the mailer can't send +config.action_mailer.raise_delivery_errors = false diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/config/environments/production.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/config/environments/production.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,18 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# The production environment is meant for finished, "live" apps. +# Code is not reloaded between requests +config.cache_classes = true + +# Use a different logger for distributed setups +# config.logger = SyslogLogger.new + +# Full error reports are disabled and caching is turned on +config.action_controller.consider_all_requests_local = false +config.action_controller.perform_caching = true + +# Enable serving of images, stylesheets, and javascripts from an asset server +# config.action_controller.asset_host = "http://assets.example.com" + +# Disable delivery errors, bad email addresses will be ignored +# config.action_mailer.raise_delivery_errors = false diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/config/environments/test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/config/environments/test.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,19 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! +config.cache_classes = true + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false + +# Tell ActionMailer not to deliver emails to the real world. +# The :test delivery method accumulates sent emails in the +# ActionMailer::Base.deliveries array. +config.action_mailer.delivery_method = :test \ No newline at end of file diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/config/routes.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/config/routes.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,23 @@ +ActionController::Routing::Routes.draw do |map| + # The priority is based upon order of creation: first created -> highest priority. + + # Sample of regular route: + # map.connect 'products/:id', :controller => 'catalog', :action => 'view' + # Keep in mind you can assign values other than :controller and :action + + # Sample of named route: + # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase' + # This route can be invoked with purchase_url(:id => product.id) + + # You can have the root of your site routed by hooking up '' + # -- just remember to delete public/index.html. + # map.connect '', :controller => "welcome" + + # Allow downloading Web Service WSDL as a file with an extension + # instead of a file named 'wsdl' + map.connect ':controller/service.wsdl', :action => 'wsdl' + + # Install the default route as the lowest priority. + map.connect ':controller/:action/:id.:format' + map.connect ':controller/:action/:id' +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/db/migrate/001_create_rooms_and_people.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/db/migrate/001_create_rooms_and_people.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,21 @@ +class CreateRoomsAndPeople < ActiveRecord::Migration + def self.up + create_table :people do |t| + t.column :name, :string + end + create_table :rooms do |t| + t.column :name, :string + t.column :maximum_occupancy, :integer + end + create_table :people_rooms do |t| + t.column :person_id, :integer + t.column :room_id, :integer + end + end + + def self.down + drop_table :people + drop_table :rooms + drop_table :people_rooms + end +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/db/schema.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/db/schema.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,21 @@ +# This file is autogenerated. Instead of editing this file, please use the +# migrations feature of ActiveRecord to incrementally modify your database, and +# then regenerate this schema definition. + +ActiveRecord::Schema.define(:version => 1) do + + create_table "people", :force => true do |t| + t.column "name", :string + end + + create_table "people_rooms", :force => true do |t| + t.column "person_id", :integer + t.column "room_id", :integer + end + + create_table "rooms", :force => true do |t| + t.column "name", :string + t.column "maximum_occupancy", :integer + end + +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/log/development.log --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/log/development.log Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,14 @@ + SQL (0.000761) SELECT * FROM schema_info + SQL (0.000436) SELECT name FROM sqlite_master WHERE type = 'table' + SQL (0.000085) PRAGMA index_list(people) + SQL (0.000082) PRAGMA index_list(people_rooms) + SQL (0.000083) PRAGMA index_list(rooms) + SQL (0.000000) SQLite3::SQLException: no such table: people: DROP TABLE people + SQL (0.029406) CREATE TABLE people ("id" INTEGER PRIMARY KEY NOT NULL, "name" varchar(255) DEFAULT NULL)  + SQL (0.000000) SQLite3::SQLException: no such table: people_rooms: DROP TABLE people_rooms + SQL (0.002897) CREATE TABLE people_rooms ("id" INTEGER PRIMARY KEY NOT NULL, "person_id" integer DEFAULT NULL, "room_id" integer DEFAULT NULL)  + SQL (0.000000) SQLite3::SQLException: no such table: rooms: DROP TABLE rooms + SQL (0.002889) CREATE TABLE rooms ("id" INTEGER PRIMARY KEY NOT NULL, "name" varchar(255) DEFAULT NULL, "maximum_occupancy" integer DEFAULT NULL)  + SQL (0.003147) CREATE TABLE schema_info (version integer) + SQL (0.004004) INSERT INTO schema_info (version) VALUES(0) + SQL (0.002317) UPDATE schema_info SET version = 1 diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/log/test.log --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/log/test.log Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,13 @@ + Person Load (0.000348) SELECT * FROM people WHERE (people."id" = 1)  + Person Load (0.000261) SELECT * FROM people WHERE (people."id" = 2)  + Room Count (0.000260) select count(*) from people_rooms + SQL (0.000425) INSERT INTO rooms ("name", "maximum_occupancy") VALUES(NULL, 2) + SQL (0.000256) INSERT INTO people_rooms ("id", "room_id", "person_id") VALUES (1, 1, 1) + SQL (0.000152) INSERT INTO people_rooms ("id", "room_id", "person_id") VALUES (2, 1, 2) + Room Count (0.000213) select count(*) from people_rooms + Person Load (0.000224) SELECT * FROM people WHERE (people."id" = 3)  + Room Count (0.000183) select count(*) from people_rooms + Room Count (0.000185) select count(*) from people_rooms + Room Load (0.000237) SELECT * FROM rooms WHERE (rooms."id" = 1)  + Person Load (0.000560) SELECT * FROM people INNER JOIN people_rooms ON people.id = people_rooms.person_id WHERE (people_rooms.room_id = 1 )  + Room Count (0.000185) select count(*) from people_rooms diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/test/fixtures/people.yml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/test/fixtures/people.yml Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,7 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +person1: + id: 1 +person2: + id: 2 +person3: + id: 3 diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/test/fixtures/rooms.yml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/test/fixtures/rooms.yml Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +first: + id: 1 +another: + id: 2 diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/test/test_helper.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/test/test_helper.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,28 @@ +ENV["RAILS_ENV"] = "test" +require File.expand_path(File.dirname(__FILE__) + "/../config/environment") +require 'test_help' + +class Test::Unit::TestCase + # Transactional fixtures accelerate your tests by wrapping each test method + # in a transaction that's rolled back on completion. This ensures that the + # test database remains unchanged so your fixtures don't have to be reloaded + # between every test method. Fewer database queries means faster tests. + # + # Read Mike Clark's excellent walkthrough at + # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting + # + # Every Active Record database supports transactions except MyISAM tables + # in MySQL. Turn off transactional fixtures in this case; however, if you + # don't care one way or the other, switching from MyISAM to InnoDB tables + # is recommended. + self.use_transactional_fixtures = true + + # Instantiated fixtures are slow, but give you @david where otherwise you + # would need people(:david). If you don't want to migrate your existing + # test cases which use the @david style and don't mind the speed hit (each + # instantiated fixtures translates to a database query per test method), + # then set this back to true. + self.use_instantiated_fixtures = false + + # Add more helper methods to be used by all tests here... +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/test/unit/room_maximum_occupancy_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/test/unit/room_maximum_occupancy_test.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,56 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class RoomMaximumOccupancyTest < Test::Unit::TestCase + fixtures :people + +# def test_1 +# room = Room.new(:maximum_occupancy => 4) +# room.maximum_occupancy = 10 +# +# assert_equal 10, room.maximum_occupancy # Still invalid +# assert_equal false, room.save +# assert_equal "You can't have the maximum set so high", room.errors.on(:maximum_occupancy) +# assert_equal 10, room.maximum_occupancy # Still invalid +# end + + def test_maximum_occupancy + room = Room.new(:maximum_occupancy => 2) + assert_equal [], room.people + assert_equal [], room.people_without_deferred_save + assert_not_equal room.unsaved_people.object_id, + room.people_without_deferred_save.object_id + + assert_nothing_raised { room.people << people(:person1) } + assert_nothing_raised { room.people << people(:person2) } + assert_equal 0, Room.count_by_sql("select count(*) from people_rooms") # Still not saved to the association table! + assert_equal 0, room.people_without_deferred_save.size + assert_equal 2, room.people.size # 2 because this looks at unsaved_people + + assert room.save # Only here is it actually saved to the association table! + assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") + assert_equal 2, room.people.size + assert_equal 2, room.people_without_deferred_save.size + + assert_nothing_raised { room.people << people(:person3) } + assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") # person3 is not yet saved to the association table + assert_equal false, room.valid? + assert_equal "There are too many people in this room", room.errors.on(:people) + + assert_equal false, room.save + assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") # It's still not there, because it didn't pass the validation. + assert_equal "There are too many people in this room", room.errors.on(:people) + assert_equal 3, room.people.size # Just like with normal attributes that fail validation... the attribute still contains the invalid data but we refuse to save until it is changed to something that is *valid*. + + room.reload + assert_equal 2, room.people.size + assert_equal 2, room.people_without_deferred_save.size + + assert_nothing_raised { room.people << people(:person3) } + assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") # person3 is not yet saved to the association table + + # If they try to go around our accessors and use the original accessors, then (and only then) will the exception be raised in before_adding_person... + assert_raise RuntimeError do + room.people_without_deferred_save << people(:person3) + end + end +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/test/unit/room_maximum_occupancy_test_1.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/test/unit/room_maximum_occupancy_test_1.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,26 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class RoomMaximumOccupancyTest < Test::Unit::TestCase + fixtures :people + + def test_maximum_occupancy + room = Room.new(:maximum_occupancy => 2) + assert_equal 0, Room.count_by_sql("select count(*) from people_rooms") + assert_equal 0, room.people.size + + room.people << people(:person1) + room.people << people(:person2) + assert room.save + assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") + assert_equal 2, room.people.size + + room.people << people(:person3) + #assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") # FAILS because it saves it in people_rooms before we even call room.save ! + + assert_equal false, room.save + # Good, it has the error ... + assert_equal "There are too many people in this room", room.errors.on(:people) + # ... but it's too late. It didn't prevent the invalid data from getting in there! + #assert_equal 2, room.people.size # FAILS + end +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/test/unit/room_maximum_occupancy_test_2.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/test/unit/room_maximum_occupancy_test_2.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,31 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class RoomMaximumOccupancyTest < Test::Unit::TestCase + fixtures :people + + def test_maximum_occupancy_using_build + room = Room.new(:maximum_occupancy => 2) + assert_equal 0, Room.count_by_sql("select count(*) from people_rooms") + assert_equal 0, room.people.size + + room.people.build(:name => 'person1') + room.people.build(:name => 'person2') + assert room.save + assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") + assert_equal 2, room.people.size + + room.people.build(:name => 'person3') + # Good, it prevented it from being saved to the database ... + assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") + # ... but it still added it to the collection stored in memory! + #assert_equal 2, room.people.size # Still FAILs. It thinks it has 3, even though the 3rd one is invalid. + + assert_equal false, room.save + assert_equal "There are too many people in this room", room.errors.on(:people) + + # If we reload from what is stored in memory, it will still just have the 2 valid people... + room.reload + assert_equal 2, room.people.size + end + +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/test/unit/room_maximum_occupancy_test_3.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/test/unit/room_maximum_occupancy_test_3.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,33 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class RoomMaximumOccupancyTest < Test::Unit::TestCase + fixtures :people + + def test_maximum_occupancy + room = Room.new(:maximum_occupancy => 2) + assert_equal 0, Room.count_by_sql("select count(*) from people_rooms") + assert_equal 0, room.people.size + + assert_nothing_raised { room.people << people(:person1) } + assert_nothing_raised { room.people << people(:person2) } + assert room.save + assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") + assert_equal 2, room.people.size + + assert_raise RuntimeError do + room.people << people(:person3) + end + assert_equal 2, Room.count_by_sql("select count(*) from people_rooms") + + assert_equal "There are too many people in this room", room.errors.on(:people) # Passes (for now!) + + # But as soon as I go to save it, it clears out the errors array!! Arg! + room.save + #assert_equal "There are too many people in this room", room.errors.on(:people) # FAILS + + #assert_equal false, room.valid? # FAILS + #assert_equal false, room.save # FAILS + assert_equal 2, room.people.size + end + +end diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/rails_root/vendor/plugins/has_and_belong_to_many_with_deferred_save/init.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/rails_root/vendor/plugins/has_and_belong_to_many_with_deferred_save/init.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,8 @@ +# This tricks the test app into loading the plugin from its parent directory. +# The alternatives migth be: +# * creating a symlink from plugin_name/rails_root/vendor/plugins/plugin_name to plugin_name +# * creating an svn:external at plugin_name/rails_root/vendor/plugins/plugin_name that pulls in the contents of plugin_name (not only would that create a circular dependency, it also would mean that changes you made to plugin_name wouldn't show up in rails_root until you committed and updated) + +#require "#{RAILS_ROOT}/../init.rb" +init_path = "#{RAILS_ROOT}/../../init.rb" +silence_warnings { eval(IO.read(init_path), binding, init_path) } diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 test/test_helper.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/test_helper.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,12 @@ +require 'rubygems' +require 'facets/core/kernel/require_local' + +# This loads the application's (default) test_helper, which loads the environment, etc. +require_local 'rails_root/test/test_helper' + +# This puts our working directory into the root of our test app. +Dir.chdir File.dirname(__FILE__) + 'rails_root/' + +# Side effect you should be aware of: +# rake_test_loader will have trouble finding your tests due to this chdir... + diff -r c7287bf557dd8af22fd417f13d894fcb511c8f0c -r a0ab430243c79b1311925cef5745c2becdbdfb03 uninstall.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/uninstall.rb Thu Feb 25 10:11:12 2010 -0800 @@ -0,0 +1,1 @@ +# Uninstall hook code here