diff --git a/tracks/Rakefile b/tracks/Rakefile index 3bb0e859..494c7b6f 100644 --- a/tracks/Rakefile +++ b/tracks/Rakefile @@ -8,3 +8,9 @@ require 'rake/testtask' require 'rake/rdoctask' require 'tasks/rails' + +begin + require 'test/rails/rake_tasks' +rescue LoadError => e + #It's ok if you don't have ZenTest installed if you're not a developer +end \ No newline at end of file diff --git a/tracks/app/controllers/application.rb b/tracks/app/controllers/application.rb index 00a77bc0..a19048da 100644 --- a/tracks/app/controllers/application.rb +++ b/tracks/app/controllers/application.rb @@ -7,7 +7,7 @@ require "redcloth" require 'date' require 'time' -Tag # We need this in development mode, or you get 'method missing' errors +#Tag # We need this in development mode, or you get 'method missing' errors class ApplicationController < ActionController::Base diff --git a/tracks/app/controllers/todos_controller.rb b/tracks/app/controllers/todos_controller.rb index 920ede02..2adaf02c 100644 --- a/tracks/app/controllers/todos_controller.rb +++ b/tracks/app/controllers/todos_controller.rb @@ -5,7 +5,7 @@ class TodosController < ApplicationController skip_before_filter :login_required, :only => [:index] prepend_before_filter :login_or_feed_token_required, :only => [:index] append_before_filter :init, :except => [ :destroy, :completed, :completed_archive, :check_deferred ] - append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :show, :update, :destroy ] + append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :toggle_star, :show, :update, :destroy ] session :off, :only => :index, :if => Proc.new { |req| is_feed_request(req) } @@ -138,6 +138,11 @@ class TodosController < ApplicationController end end end + + def toggle_star + @todo.toggle_star! + @saved = @todo.save! + end def update @todo.tag_with(params[:tag_list],@user) if params[:tag_list] diff --git a/tracks/app/helpers/todos_helper.rb b/tracks/app/helpers/todos_helper.rb index 88b550a5..bdf5aa55 100644 --- a/tracks/app/helpers/todos_helper.rb +++ b/tracks/app/helpers/todos_helper.rb @@ -24,6 +24,17 @@ module TodosHelper str end + def remote_star_icon + str = link_to( image_tag_for_star(@todo), + toggle_star_todo_path(@todo), + :class => "icon star_item", :title => "star the action '#{@todo.description}'") + apply_behavior '.item-container a.star_item:click', + remote_function(:url => javascript_variable('this.href'), :method => 'put', + :with => "{ _source_view : '#{@source_view}' }"), + :prevent_default => true + str + end + def remote_edit_icon if !@todo.completed? str = link_to( image_tag_for_edit, @@ -41,8 +52,8 @@ module TodosHelper def remote_toggle_checkbox str = check_box_tag('item_id', toggle_check_todo_path(@todo), @todo.completed?, :class => 'item-checkbox') apply_behavior '.item-container input.item-checkbox:click', - remote_function(:url => javascript_variable('this.value'), - :with => "{ method : 'post', _source_view : '#{@source_view}' }") + remote_function(:url => javascript_variable('this.value'), :method => 'put', + :with => "{ _source_view : '#{@source_view}' }") str end @@ -57,7 +68,8 @@ module TodosHelper end def tag_list - @todo.tags.collect{|t| "" + link_to(t.name, :action => "tag", :id => t.name) + ""}.join('') + tag_list = @todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME}.collect{|t| "" + link_to(t.name, :action => "tag", :id => t.name) + ""}.join('') + "#{tag_list}" end def deferred_due_date @@ -204,4 +216,9 @@ module TodosHelper image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(@todo, 'edit_icon')) end + def image_tag_for_star(todo) + class_str = todo.starred? ? "starred_todo" : "unstarred_todo" + image_tag("blank.png", :title =>"Star action", :class => class_str) + end + end diff --git a/tracks/app/models/todo.rb b/tracks/app/models/todo.rb index fc85b878..8e853fcd 100644 --- a/tracks/app/models/todo.rb +++ b/tracks/app/models/todo.rb @@ -4,6 +4,8 @@ class Todo < ActiveRecord::Base belongs_to :project belongs_to :user + STARRED_TAG_NAME = "starred" + acts_as_state_machine :initial => :active, :column => 'state' state :active, :enter => Proc.new { |t| t[:show_from] = nil } @@ -89,5 +91,21 @@ class Todo < ActiveRecord::Base :description => "Actions for #{user.display_name}" } end + + + def starred? + tags.any? {|tag| tag.name == STARRED_TAG_NAME} + end + + def toggle_star! + if starred? + delete_tags STARRED_TAG_NAME + tags.reload + else + add_tag STARRED_TAG_NAME + tags.reload + end + starred? + end end \ No newline at end of file diff --git a/tracks/app/views/todos/_todo.rhtml b/tracks/app/views/todos/_todo.rhtml index ce349a6a..7815a462 100644 --- a/tracks/app/views/todos/_todo.rhtml +++ b/tracks/app/views/todos/_todo.rhtml @@ -1,11 +1,11 @@ <% @todo = todo - Tag %>
<%= remote_delete_icon %> <%= remote_edit_icon %> + <%= remote_star_icon %> <%= remote_toggle_checkbox unless source_view_is :deferred %>
<%= date_span %> diff --git a/tracks/app/views/todos/toggle_star.rjs b/tracks/app/views/todos/toggle_star.rjs new file mode 100644 index 00000000..a36b8ed0 --- /dev/null +++ b/tracks/app/views/todos/toggle_star.rjs @@ -0,0 +1,3 @@ +if @saved + page[@todo].down('a.star_item').down('img').toggleClassName('starred_todo').toggleClassName('unstarred_todo') +end \ No newline at end of file diff --git a/tracks/config/routes.rb b/tracks/config/routes.rb index 130277ba..ec198109 100644 --- a/tracks/config/routes.rb +++ b/tracks/config/routes.rb @@ -33,7 +33,7 @@ ActionController::Routing::Routes.draw do |map| # ToDo Routes map.resources :todos, - :member => {:toggle_check => :post}, + :member => {:toggle_check => :put, :toggle_star => :put}, :collection => {:check_deferred => :post, :filter_to_context => :post, :filter_to_project => :post} map.with_options :controller => "todos" do |todos| todos.home '', :action => "index" diff --git a/tracks/lib/activerecord_base_tag_extensions.rb b/tracks/lib/activerecord_base_tag_extensions.rb index b3443cbc..e36ac69d 100644 --- a/tracks/lib/activerecord_base_tag_extensions.rb +++ b/tracks/lib/activerecord_base_tag_extensions.rb @@ -22,5 +22,9 @@ class ActiveRecord::Base split = tag_string.downcase.split(", ") tags.delete tags.select{|t| split.include? t.name} end + + def add_tag tag_name + Tag.find_or_create_by_name(tag_name).on(self,user) + end end \ No newline at end of file diff --git a/tracks/public/images/staricons.png b/tracks/public/images/staricons.png new file mode 100644 index 00000000..c4b6618e Binary files /dev/null and b/tracks/public/images/staricons.png differ diff --git a/tracks/public/stylesheets/standard.css b/tracks/public/stylesheets/standard.css index 4cab57b3..752ada4b 100644 --- a/tracks/public/stylesheets/standard.css +++ b/tracks/public/stylesheets/standard.css @@ -78,6 +78,11 @@ a:hover img.edit_item {background-image: url(../images/edit_on.png); background- img.delete_item {background-image: url(../images/delete_off.png); background-repeat: no-repeat; border: none;} a:hover img.delete_item {background-image: url(../images/delete_on.png);background-color: transparent;background-repeat: no-repeat; border: none;} +img.starred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: 0px 0px;} +a:hover img.starred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: -16px 0px;} +img.unstarred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: -32px 0px;} +a:hover img.unstarred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: -48px 0px;} + a.to_top {background: transparent url(../images/top_off.png) no-repeat;} a.to_top:hover {background: transparent url(../images/top_on.png) no-repeat;} @@ -285,12 +290,12 @@ input.item-checkbox { } .description { - margin-left: 70px; + margin-left: 85px; } .stale_l1, .stale_l2, .stale_l3 { - margin-left: 67px; - padding-left: 3px; + margin-left: 82px; + padding-left: 3px; } .stale_l1 { diff --git a/tracks/test/functional/backend_controller_test.rb b/tracks/test/functional/backend_controller_test.rb index a42ae13c..d795c5b7 100644 --- a/tracks/test/functional/backend_controller_test.rb +++ b/tracks/test/functional/backend_controller_test.rb @@ -4,7 +4,7 @@ require 'backend_controller' # Re-raise errors caught by the controller. class BackendController; def rescue_action(e) raise e end; end -class BackendControllerTest < Test::Unit::TestCase +class BackendControllerTest < Test::Rails::TestCase fixtures :users, :projects, :contexts, :todos, :notes def setup diff --git a/tracks/test/functional/contexts_controller_test.rb b/tracks/test/functional/contexts_controller_test.rb index 0f384825..71bedb88 100644 --- a/tracks/test/functional/contexts_controller_test.rb +++ b/tracks/test/functional/contexts_controller_test.rb @@ -150,4 +150,36 @@ class ContextsControllerTest < TodoContainerControllerTestBase assert_response :ok end + def test_show_sets_title + @request.session['user_id'] = users(:admin_user).id + get :show, { :id => "1" } + assert_equal 'TRACKS::Context: agenda', assigns['page_title'] + end + + def test_show_renders_show_template + @request.session['user_id'] = users(:admin_user).id + get :show, { :id => "1" } + assert_template "contexts/show" + end + + def test_show_xml_renders_context_to_xml + @request.session['user_id'] = users(:admin_user).id + get :show, { :id => "1", :format => 'xml' } + assert_equal contexts(:agenda).to_xml( :except => :user_id ), @response.body + end + + def test_show_with_nil_context_returns_404 + @request.session['user_id'] = users(:admin_user).id + get :show, { :id => "0" } + assert_equal 'Context not found', @response.body + assert_response 404 + end + + def test_show_xml_with_nil_context_returns_404 + @request.session['user_id'] = users(:admin_user).id + get :show, { :id => "0", :format => 'xml' } + assert_response 404 + assert_xml_select 'error', 'Context not found' + end + end diff --git a/tracks/test/functional/data_controller_test.rb b/tracks/test/functional/data_controller_test.rb index 02c58840..4f8e36ec 100644 --- a/tracks/test/functional/data_controller_test.rb +++ b/tracks/test/functional/data_controller_test.rb @@ -4,7 +4,7 @@ require 'data_controller' # Re-raise errors caught by the controller. class DataController; def rescue_action(e) raise e end; end -class DataControllerTest < Test::Unit::TestCase +class DataControllerTest < Test::Rails::TestCase fixtures :users, :preferences, :projects, :notes def setup diff --git a/tracks/test/functional/feedlist_controller_test.rb b/tracks/test/functional/feedlist_controller_test.rb index bcf346d8..38923a82 100644 --- a/tracks/test/functional/feedlist_controller_test.rb +++ b/tracks/test/functional/feedlist_controller_test.rb @@ -4,7 +4,7 @@ require 'feedlist_controller' # Re-raise errors caught by the controller. class FeedlistController; def rescue_action(e) raise e end; end -class FeedlistControllerTest < Test::Unit::TestCase +class FeedlistControllerTest < Test::Rails::TestCase fixtures :users, :preferences, :projects, :contexts, :todos, :notes def setup diff --git a/tracks/test/functional/login_controller_test.rb b/tracks/test/functional/login_controller_test.rb index 273b2b72..a17f2100 100644 --- a/tracks/test/functional/login_controller_test.rb +++ b/tracks/test/functional/login_controller_test.rb @@ -5,7 +5,7 @@ require_dependency "login_system" # Re-raise errors caught by the controller. class LoginController; def rescue_action(e) raise e end; end -class LoginControllerTest < Test::Unit::TestCase +class LoginControllerTest < Test::Rails::TestCase fixtures :preferences, :users def setup diff --git a/tracks/test/functional/notes_controller_test.rb b/tracks/test/functional/notes_controller_test.rb index 9100fbf4..4804d4e3 100644 --- a/tracks/test/functional/notes_controller_test.rb +++ b/tracks/test/functional/notes_controller_test.rb @@ -4,7 +4,7 @@ require 'notes_controller' # Re-raise errors caught by the controller. class NotesController; def rescue_action(e) raise e end; end -class NotesControllerTest < Test::Unit::TestCase +class NotesControllerTest < Test::Rails::TestCase def setup @controller = NotesController.new request = ActionController::TestRequest.new diff --git a/tracks/test/functional/preferences_controller_test.rb b/tracks/test/functional/preferences_controller_test.rb index 1add241f..fbc397c3 100644 --- a/tracks/test/functional/preferences_controller_test.rb +++ b/tracks/test/functional/preferences_controller_test.rb @@ -5,7 +5,7 @@ require 'preference' # Re-raise errors caught by the controller. class PreferencesController; def rescue_action(e) raise e end; end -class PreferencesControllerTest < Test::Unit::TestCase +class PreferencesControllerTest < Test::Rails::TestCase fixtures :users def setup diff --git a/tracks/test/functional/projects_controller_test.rb b/tracks/test/functional/projects_controller_test.rb index c3aa0564..3173c3ea 100644 --- a/tracks/test/functional/projects_controller_test.rb +++ b/tracks/test/functional/projects_controller_test.rb @@ -57,7 +57,7 @@ class ProjectsControllerTest < TodoContainerControllerTestBase def test_create_project_and_go_to_project_page num_projects = Project.count xhr :post, :create, { :project => {:name => 'Immediate Project Planning Required'}, :go_to_project => 1} - assert_js_redirected_to '/projects/5' + assert_js_redirected_to %r{/?projects/\d+} assert_equal num_projects + 1, Project.count end diff --git a/tracks/test/functional/todo_container_controller_test_base.rb b/tracks/test/functional/todo_container_controller_test_base.rb index 28d27026..c4436adb 100644 --- a/tracks/test/functional/todo_container_controller_test_base.rb +++ b/tracks/test/functional/todo_container_controller_test_base.rb @@ -1,4 +1,4 @@ -class TodoContainerControllerTestBase < Test::Unit::TestCase +class TodoContainerControllerTestBase < Test::Rails::TestCase def perform_setup(container_class, controller_class) @controller = controller_class.new diff --git a/tracks/test/functional/todos_controller_test.rb b/tracks/test/functional/todos_controller_test.rb index ad410fc0..171c2b24 100644 --- a/tracks/test/functional/todos_controller_test.rb +++ b/tracks/test/functional/todos_controller_test.rb @@ -4,7 +4,7 @@ require 'todos_controller' # Re-raise errors caught by the controller. class TodosController; def rescue_action(e) raise e end; end -class TodosControllerTest < Test::Unit::TestCase +class TodosControllerTest < Test::Rails::TestCase fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings def setup @@ -25,6 +25,15 @@ class TodosControllerTest < Test::Unit::TestCase assert_equal 1, assigns['context_not_done_counts'][contexts(:lab).id] end + def test_tag_is_retrieved_properly + @request.session['user_id'] = users(:admin_user).id + get :index + t = assigns['not_done_todos'].find{|t| t.id == 2} + assert_equal 1, t.tags.count + assert_equal 'foo', t.tags[0].name + assert !t.starred? + end + def test_not_done_counts_after_hiding_project p = Project.find(1) p.hide! diff --git a/tracks/test/functional/users_controller_test.rb b/tracks/test/functional/users_controller_test.rb index f6a95a3c..5c19264f 100644 --- a/tracks/test/functional/users_controller_test.rb +++ b/tracks/test/functional/users_controller_test.rb @@ -4,7 +4,7 @@ require 'users_controller' # Re-raise errors caught by the controller. class UsersController; def rescue_action(e) raise e end; end -class UsersControllerTest < Test::Unit::TestCase +class UsersControllerTest < Test::Rails::TestCase fixtures :preferences, :users def setup diff --git a/tracks/test/selenium/home/star_todo.rsel b/tracks/test/selenium/home/star_todo.rsel new file mode 100644 index 00000000..00300e1d --- /dev/null +++ b/tracks/test/selenium/home/star_todo.rsel @@ -0,0 +1,5 @@ +setup :fixtures => :all +login :as => 'admin' +open '/' +click "css=#c1 #todo_9 a.star_item" +wait_for_element_present "css=#c1 #todo_9 img.starred_todo" diff --git a/tracks/test/selenium/home/verify_item_not_starred.rsel b/tracks/test/selenium/home/verify_item_not_starred.rsel new file mode 100644 index 00000000..5358a3d9 --- /dev/null +++ b/tracks/test/selenium/home/verify_item_not_starred.rsel @@ -0,0 +1,4 @@ +setup :fixtures => :all +login :as => 'admin' +open '/' +assert_element_present "css=#c2 #todo_2 img.unstarred_todo" diff --git a/tracks/test/selenium/project_detail/activate_deferred_todo.rsel b/tracks/test/selenium/project_detail/activate_deferred_todo.rsel index 3740bf31..34109427 100644 --- a/tracks/test/selenium/project_detail/activate_deferred_todo.rsel +++ b/tracks/test/selenium/project_detail/activate_deferred_todo.rsel @@ -1,13 +1,20 @@ setup :fixtures => :all -next_available_todo_id = 18 login :as => 'admin' + +#first, defer a todo open "/projects/1" -include_partial 'project_detail/add_deferred_todo' -open "/projects/1" -click "edit_icon_todo_#{next_available_todo_id}" -wait_for_element_present "show_from_todo_#{next_available_todo_id}" -type "show_from_todo_#{next_available_todo_id}", "" +click "edit_icon_todo_5" +wait_for_element_present "show_from_todo_5" +type "show_from_todo_5", "1/1/2030" click "//input[@value='Update']" -wait_for_element_present "xpath=//div[@id='p1'] //div[@id='todo_#{next_available_todo_id}']" +wait_for_element_present "xpath=//div[@id='tickler'] //div[@id='todo_5']" + +#now activate the other deferred one +open "/projects/1" +click "edit_icon_todo_15" +wait_for_element_present "show_from_todo_15" +type "show_from_todo_15", "" +click "//input[@value='Update']" +wait_for_element_present "xpath=//div[@id='p1'] //div[@id='todo_15']" assert_not_visible "tickler-empty-nd" -assert_text 'badge_count', '3' +assert_text 'badge_count', '2' diff --git a/tracks/test/test_helper.rb b/tracks/test/test_helper.rb index 5ec9b469..9e60d452 100644 --- a/tracks/test/test_helper.rb +++ b/tracks/test/test_helper.rb @@ -1,7 +1,10 @@ ENV["RAILS_ENV"] = "test" require File.expand_path(File.dirname(__FILE__) + "/../config/environment") +require File.expand_path(File.dirname(__FILE__) + "/../app/controllers/application") +require 'test/rails' #you need the zentest gem isntalled require 'test_help' - +require 'flexmock/test_unit' #and the flexmock gem, too! + module Tracks class Config def self.salt @@ -11,6 +14,20 @@ module Tracks end class Test::Unit::TestCase + + def xml_document + @xml_document ||= HTML::Document.new(@response.body, false, true) + end + + def assert_xml_select(*args, &block) + @html_document = xml_document + assert_select(*args, &block) + end + +end + +class Test::Rails::TestCase < Test::Unit::TestCase + # Turn off transactional fixtures if you're working with MyISAM tables in MySQL self.use_transactional_fixtures = true @@ -30,16 +47,7 @@ class Test::Unit::TestCase end return string end - - def xml_document - @xml_document ||= HTML::Document.new(@response.body, false, true) - end - - def assert_xml_select(*args, &block) - @html_document = xml_document - assert_select(*args, &block) - end - + def next_week 1.week.from_now.utc.to_date end @@ -58,6 +66,8 @@ class Test::Unit::TestCase if options.is_a?(String) assert_equal(options.gsub(/^\//, ''), redirected_to, message) + elsif options.is_a?(Regexp) + assert(options =~ redirected_to, "#{message} #{options} #{redirected_to}") else msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is )", redirected_to) assert_equal(@controller.url_for(options).match(js_regexp)[3], redirected_to, msg) @@ -69,7 +79,7 @@ class Test::Unit::TestCase end class ActionController::IntegrationTest - + def assert_test_environment_ok assert_equal "test", ENV['RAILS_ENV'] assert_equal "change-me", Tracks::Config.salt diff --git a/tracks/test/unit/context_test.rb b/tracks/test/unit/context_test.rb index 3ffc71a0..4952d8c6 100644 --- a/tracks/test/unit/context_test.rb +++ b/tracks/test/unit/context_test.rb @@ -1,6 +1,6 @@ require File.dirname(__FILE__) + '/../test_helper' -class ContextTest < Test::Unit::TestCase +class ContextTest < Test::Rails::TestCase fixtures :contexts, :todos, :users, :preferences def setup @@ -107,4 +107,11 @@ class ContextTest < Test::Unit::TestCase assert_equal "

#{undone_todo_count}. Context is Hidden.

", @agenda.summary(undone_todo_count) end + def test_null_object + c = Context.null_object + assert c.nil? + assert_nil c.id + assert_equal '', c.name + end + end diff --git a/tracks/test/unit/notes_test.rb b/tracks/test/unit/notes_test.rb index de4354f9..c9691c62 100644 --- a/tracks/test/unit/notes_test.rb +++ b/tracks/test/unit/notes_test.rb @@ -1,6 +1,6 @@ require File.dirname(__FILE__) + '/../test_helper' -class NotesTest < Test::Unit::TestCase +class NotesTest < Test::Rails::TestCase fixtures :notes def setup diff --git a/tracks/test/unit/preference_test.rb b/tracks/test/unit/preference_test.rb index 269d870f..61c1654f 100644 --- a/tracks/test/unit/preference_test.rb +++ b/tracks/test/unit/preference_test.rb @@ -1,6 +1,6 @@ require File.dirname(__FILE__) + '/../test_helper' -class PreferenceTest < Test::Unit::TestCase +class PreferenceTest < Test::Rails::TestCase fixtures :users, :preferences def setup diff --git a/tracks/test/unit/project_test.rb b/tracks/test/unit/project_test.rb index 81186d87..52aa752a 100644 --- a/tracks/test/unit/project_test.rb +++ b/tracks/test/unit/project_test.rb @@ -1,6 +1,6 @@ require File.dirname(__FILE__) + '/../test_helper' -class ProjectTest < Test::Unit::TestCase +class ProjectTest < Test::Rails::TestCase fixtures :projects, :contexts, :todos, :users, :preferences def setup diff --git a/tracks/test/unit/tag_test.rb b/tracks/test/unit/tag_test.rb index 6238c1eb..031d8bb7 100644 --- a/tracks/test/unit/tag_test.rb +++ b/tracks/test/unit/tag_test.rb @@ -1,6 +1,6 @@ require File.dirname(__FILE__) + '/../test_helper' -class TagTest < Test::Unit::TestCase +class TagTest < Test::Rails::TestCase fixtures :tags # Replace this with your real tests. diff --git a/tracks/test/unit/tagging_test.rb b/tracks/test/unit/tagging_test.rb index 35dd3c4b..e33fd40b 100644 --- a/tracks/test/unit/tagging_test.rb +++ b/tracks/test/unit/tagging_test.rb @@ -1,6 +1,6 @@ require File.dirname(__FILE__) + '/../test_helper' -class TaggingTest < Test::Unit::TestCase +class TaggingTest < Test::Rails::TestCase fixtures :taggings # Replace this with your real tests. diff --git a/tracks/test/unit/todo_test.rb b/tracks/test/unit/todo_test.rb index 0ede3f20..2aaea719 100644 --- a/tracks/test/unit/todo_test.rb +++ b/tracks/test/unit/todo_test.rb @@ -1,8 +1,8 @@ require File.dirname(__FILE__) + '/../test_helper' require 'date' -class TodoTest < Test::Unit::TestCase - fixtures :todos, :users, :contexts, :preferences +class TodoTest < Test::Rails::TestCase + fixtures :todos, :users, :contexts, :preferences, :tags, :taggings def setup @not_completed1 = Todo.find(1).reload @@ -139,4 +139,28 @@ class TodoTest < Test::Unit::TestCase assert_equal :deferred, t.current_state end + def test_todo_is_not_starred + assert !@not_completed1.starred? + end + + def test_todo_2_is_not_starred + assert !Todo.find(2).starred? + end + + def test_todo_is_starred_after_starred_tag_is_added + @not_completed1.add_tag('starred') + assert @not_completed1.starred? + end + + def test_todo_is_starred_after_toggle_starred + @not_completed1.toggle_star! + assert @not_completed1.starred? + end + + def test_todo_is_not_starred_after_toggle_starred_twice + @not_completed1.toggle_star! + @not_completed1.toggle_star! + assert !@not_completed1.starred? + end + end diff --git a/tracks/test/unit/user_test.rb b/tracks/test/unit/user_test.rb index 8db1cbca..34a07606 100644 --- a/tracks/test/unit/user_test.rb +++ b/tracks/test/unit/user_test.rb @@ -16,7 +16,7 @@ class SimpleLdapAuthenticator end end -class UserTest < Test::Unit::TestCase +class UserTest < Test::Rails::TestCase fixtures :users, :preferences, :projects, :contexts, :todos def setup diff --git a/tracks/test/views/todos_helper_test.rb b/tracks/test/views/todos_helper_test.rb new file mode 100644 index 00000000..0a7245b9 --- /dev/null +++ b/tracks/test/views/todos_helper_test.rb @@ -0,0 +1,40 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class TodosHelperTest < Test::Rails::HelperTestCase + + def setup + super + end + + include TodosHelper + + def test_remote_star_icon_unstarred + @todo = flexmock(:id => 1, :to_param => 1, :description => 'Get gas', :starred? => false) + assert_remote_star_icon_helper_matches %r{Blank} + assert_behavior_registered + end + + def test_remote_star_icon_starred + @todo = flexmock(:id => 1, :to_param => 1, :description => 'Get gas', :starred? => true) + assert_remote_star_icon_helper_matches %r{Blank} + assert_behavior_registered + end + + def assert_remote_star_icon_helper_matches(regex) + @controller.send :initialise_js_behaviours #simulate before filter + output = remote_star_icon + #puts output + assert output =~ regex + @controller.send :store_js_behaviours #simulate after filter + end + + def assert_behavior_registered + behaviors = @controller.session[:js_behaviours] + assert behaviors[:options][:reapply_after_ajax] + assert_equal 1, behaviors[:rules].length + rule = behaviors[:rules][0] + assert_equal ".item-container a.star_item:click", rule[0] + assert_equal "new Ajax.Request(this.href, {asynchronous:true, evalScripts:true, method:'put', parameters:{ _source_view : '' }}); return false;", + rule[1] + end +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/Manifest b/tracks/vendor/plugins/has_many_polymorphs/Manifest new file mode 100644 index 00000000..69ff049f --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/Manifest @@ -0,0 +1,43 @@ +LICENSE +Manifest +README +Rakefile +init.rb +lib/has_many_polymorphs/association.rb +lib/has_many_polymorphs/autoload.rb +lib/has_many_polymorphs/base.rb +lib/has_many_polymorphs/class_methods.rb +lib/has_many_polymorphs/configuration.rb +lib/has_many_polymorphs/debugging_tools.rb +lib/has_many_polymorphs/dependencies.rb +lib/has_many_polymorphs/rake_task_redefine_task.rb +lib/has_many_polymorphs/reflection.rb +lib/has_many_polymorphs/support_methods.rb +lib/has_many_polymorphs.rb +test/fixtures/aquatic/fish.yml +test/fixtures/aquatic/little_whale_pupils.yml +test/fixtures/aquatic/whales.yml +test/fixtures/bow_wows.yml +test/fixtures/cats.yml +test/fixtures/eaters_foodstuffs.yml +test/fixtures/frogs.yml +test/fixtures/keep_your_enemies_close.yml +test/fixtures/petfoods.yml +test/fixtures/wild_boars.yml +test/models/aquatic/fish.rb +test/models/aquatic/pupils_whale.rb +test/models/aquatic/whale.rb +test/models/beautiful_fight_relationship.rb +test/models/cat.rb +test/models/dog.rb +test/models/eaters_foodstuff.rb +test/models/frog.rb +test/models/kitten.rb +test/models/petfood.rb +test/models/tabby.rb +test/models/wild_boar.rb +test/modules/extension_module.rb +test/modules/other_extension_module.rb +test/schema.rb +test/test_helper.rb +test/unit/polymorph_test.rb diff --git a/tracks/vendor/plugins/has_many_polymorphs/README b/tracks/vendor/plugins/has_many_polymorphs/README index 1cf509a6..af5b0ad6 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/README +++ b/tracks/vendor/plugins/has_many_polymorphs/README @@ -1,17 +1,24 @@ -Self-referential, polymorphic has_many :through helper +Self-referential, polymorphic has_many :through helper for ActiveRecord. -Copyright 2006 Evan Weaver (see the LICENSE file) +Copyright 2007 Cloudburst, LLC (see the LICENSE file) -"model :parent_class" may be required in some controllers or perhaps models in order for reloading to work properly, since the parent setup must be executed on the child every time the child class is reloaded. - -Usage and help: -http://blog.evanweaver.com/articles/2006/06/02/has_many_polymorphs - -Also see the source code, although it's probably not going to be super helpful to you. +Documentation: +http://blog.evanweaver.com/pages/has_many_polymorphs Changelog: +27.3. use new :source and :source_type options in 1.2.3 (David Lemstra); fix pluralization bug; add some tests; experimental tagging generator +27.2. deprecate has_many_polymorphs_cache_classes= option, because it doesn't really work. use config.cache_classes= instead to cache all reloadable items +27.1. dispatcher.to_prepare didn't fire in the console; now using a config.after_initialize wrapper instead +27. dependency injection framework elimates having to care about load order +26. make the logger act sane for the gem version +25.2. allow :skip_duplicates on double relationships +25.1. renamed :ignore_duplicates to :skip_duplicates to better express its non-passive behavior, made sure not to load target set on push unless necessary +25. activerecord compatibility branch becomes trunk. extra options now supported for double polymorphism; conditions nulled-out and propogated to child relationships; more tests; new :ignore_duplicates option on macro can be set to false if you want << to try to push duplicate associations +24.1. code split into multiple files. tests added for pluralization check. rails 1.1.6 no longer officially supported. +24. unlimited mixed class association extensions for both single and double targets and joins +23. gem version 22. api change; prefix on methods is now singular when using :rename_individual_collections 21. add configuration option to cache polymorphic classes in development mode 20. collection methods (push, delete, clear) now on individual collections @@ -40,7 +47,3 @@ Changelog: 3. added :dependent support on the join table 1-2. no changelog -Known problems: - -1. Plugin's test fixtures do not load properly for non-edge postgres, invalidating the tests. -2. quote_value() hack is stupid. \ No newline at end of file diff --git a/tracks/vendor/plugins/has_many_polymorphs/Rakefile b/tracks/vendor/plugins/has_many_polymorphs/Rakefile new file mode 100644 index 00000000..9badf5dd --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/Rakefile @@ -0,0 +1,59 @@ +require 'rubygems' +require 'rake' +require 'lib/has_many_polymorphs/rake_task_redefine_task' + +NAME = "has_many_polymorphs" + +begin + require 'rake/clean' + gem 'echoe', '>= 1.1' + require 'echoe' + require 'fileutils' + + AUTHOR = "Evan Weaver" + EMAIL = "evan at cloudbur dot st" + DESCRIPTION = "Self-referential, polymorphic has_many :through helper for ActiveRecord." + CHANGES = `cat README`[/^([\d\.]+\. .*)/, 1] + RUBYFORGE_NAME = "polymorphs" + GEM_NAME = "has_many_polymorphs" + HOMEPATH = "http://blog.evanweaver.com" + RELEASE_TYPES = ["gem"] + REV = nil + VERS = `cat README`[/^([\d\.]+)\. /, 1] + CLEAN.include ['**/.*.sw?', '*.gem', '.config'] + RDOC_OPTS = ['--quiet', '--title', "has_many_polymorphs documentation", + "--opname", "index.html", + "--line-numbers", + "--main", "README", + "--inline-source"] + + include FileUtils + + echoe = Echoe.new(GEM_NAME, VERS) do |p| + p.author = AUTHOR + p.rubyforge_name = RUBYFORGE_NAME + p.name = NAME + p.description = DESCRIPTION + p.changes = CHANGES + p.email = EMAIL + p.summary = DESCRIPTION + p.url = HOMEPATH + p.need_tar = false + p.need_tar_gz = true + p.test_globs = ["*_test.rb"] + p.clean_globs = CLEAN + end + +rescue LoadError => boom + puts "You are missing a dependency required for meta-operations on this gem." + puts "#{boom.to_s.capitalize}." + + desc 'Run the default tasks' + task :default => :test +end + +desc 'Run the test suite.' +Rake::Task.redefine_task("test") do + puts "Notice; tests must be run from within a functioning Rails environment." + system "ruby -Ibin:lib:test test/unit/polymorph_test.rb #{ENV['METHOD'] ? "--name=#{ENV['METHOD']}" : ""}" +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/TAGGING_INSTALL b/tracks/vendor/plugins/has_many_polymorphs/TAGGING_INSTALL new file mode 100644 index 00000000..2e2ec55a --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/TAGGING_INSTALL @@ -0,0 +1,12 @@ + +Thank you for installing has_many_polymorphs! + +There is an experimental tagging system generator in place. + + ./script/generator tagging TaggableModel1 TaggableModel2 [..] + +You can use the flags --skip-migration and/or --self-referential. + +Tests will be generated, but will not work unless you have at +least 2 fixture entries for each taggable model. Their ids must +be 1 and 2. diff --git a/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/tagging_generator.rb b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/tagging_generator.rb new file mode 100644 index 00000000..b74e97c3 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/tagging_generator.rb @@ -0,0 +1,78 @@ +require 'ruby-debug' +Debugger.start + +class TaggingGenerator < Rails::Generator::NamedBase + default_options :skip_migration => false + default_options :self_referential => false + attr_reader :parent_association_name + attr_reader :taggable_models + + def initialize(runtime_args, runtime_options = {}) + @parent_association_name = (runtime_args.include?("--self-referential") ? "tagger" : "tag") + @taggable_models = runtime_args.reject{|opt| opt =~ /^--/}.map do |taggable| + ":" + taggable.underscore.pluralize + end + @taggable_models += [":tags"] if runtime_args.include?("--self-referential") + @taggable_models.uniq! + + hacks + + runtime_args.unshift("placeholder") + super + end + + def hacks + # add the extension require in environment.rb + phrase = "require 'tagging_extensions'" + filename = "#{RAILS_ROOT}/config/environment.rb" + unless (open(filename) do |file| + file.grep(/#{Regexp.escape phrase}/).any? + end) + open(filename, 'a+') do |file| + file.puts "\n" + phrase + "\n" + end + end + end + + def manifest + record do |m| + m.class_collisions class_path, class_name, "#{class_name}Test" + + m.directory File.join('app/models', class_path) + m.directory File.join('test/unit', class_path) + m.directory File.join('test/fixtures', class_path) + m.directory File.join('test/fixtures', class_path) + m.directory File.join('lib') + + m.template 'tag.rb', File.join('app/models', class_path, "tag.rb") + m.template 'tag_test.rb', File.join('test/unit', class_path, "tag_test.rb") + m.template 'tags.yml', File.join('test/fixtures', class_path, "tags.yml") + + m.template 'tagging.rb', File.join('app/models', class_path, "tagging.rb") + m.template 'tagging_test.rb', File.join('test/unit', class_path, "tagging_test.rb") + m.template 'taggings.yml', File.join('test/fixtures', class_path, "taggings.yml") + + m.template 'tagging_extensions.rb', File.join('lib', 'tagging_extensions.rb') + + unless options[:skip_migration] + m.migration_template 'migration.rb', 'db/migrate', + :migration_file_name => "create_tags_and_taggings" + end + + end + end + + protected + def banner + "Usage: #{$0} generate tagging [TaggableModelA TaggableModelB ...]" + end + + def add_options!(opt) + opt.separator '' + opt.separator 'Options:' + opt.on("--skip-migration", + "Don't generate a migration file for this model") { |v| options[:skip_migration] = v } + opt.on("--self-referential", + "Allow tags to tag themselves.") { |v| options[:self_referential] = v } + end +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/index.rhtml b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/index.rhtml new file mode 100644 index 00000000..e69de29b diff --git a/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/migration.rb b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/migration.rb new file mode 100644 index 00000000..4485c555 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/migration.rb @@ -0,0 +1,23 @@ +class CreateTagsAndTaggings < ActiveRecord::Migration + + def self.up + create_table :tags do |t| + t.column :name, :string, :null => false + end + add_index :tags, :name, :unique => true + + create_table :taggings do |t| + t.column :<%= parent_association_name -%>_id, :integer, :null => false + t.column :taggable_id, :integer, :null => false + t.column :taggable_type, :string, :null => false + # t.column :position, :integer + end + add_index :taggings, [:<%= parent_association_name -%>_id, :taggable_id, :taggable_type], :unique => true + end + + def self.down + drop_table :tags + drop_table :taggings + end + +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tag.rb b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tag.rb new file mode 100644 index 00000000..1e501191 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tag.rb @@ -0,0 +1,35 @@ +class Tag < ActiveRecord::Base + + DELIMITER = " " # how to separate tags in strings (you may + # also need to change the validates_format_of parameters + # if you update this) + + # if speed becomes an issue, you could remove these validations + # and rescue the AR index errors instead + validates_presence_of :name + validates_uniqueness_of :name, :case_sensitive => false + validates_format_of :name, :with => /^[a-zA-Z0-9\_\-]+$/, + :message => "can not contain special characters" + + has_many_polymorphs :taggables, + :from => [<%= taggable_models.join(", ") %>], + :through => :taggings, + :dependent => :destroy, +<% if options[:self_referential] -%> :as => :<%= parent_association_name -%>, +<% end -%> + :skip_duplicates => false, + :parent_extend => proc { # XXX this isn't right + def to_s + self.map(&:name).sort.join(Tag::DELIMITER) + end + } + + def before_create + # if you allow editable tag names, you might want before_save instead + self.name = name.downcase.strip.squeeze(" ") + end + + class Error < StandardError + end + +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tag_test.rb b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tag_test.rb new file mode 100644 index 00000000..8a4de87c --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tag_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class TagTest < Test::Unit::TestCase + fixtures :tags, :taggings, :recipes, :posts + + def test_to_s + assert_equal "delicious sexy", Recipe.find(2).tags.to_s + end + +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging.rb b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging.rb new file mode 100644 index 00000000..5263120e --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging.rb @@ -0,0 +1,15 @@ +class Tagging < ActiveRecord::Base + + belongs_to :<%= parent_association_name -%><%= ", :foreign_key => \"#{parent_association_name}_id\", :class_name => \"Tag\"" if options[:self_referential] %> + belongs_to :taggable, :polymorphic => true + + # if you want acts_as_list, you will have to manage the tagging positions + # manually, by created decorated join records + # acts_as_list :scope => :taggable + + def before_destroy + # if all the taggings for a particular <%= parent_association_name -%> are deleted, we want to + # delete the <%= parent_association_name -%> too + <%= parent_association_name -%>.destroy_without_callbacks if <%= parent_association_name -%>.taggings.count == 1 + end +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging_extensions.rb b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging_extensions.rb new file mode 100644 index 00000000..1c992cf3 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging_extensions.rb @@ -0,0 +1,88 @@ +class ActiveRecord::Base + + # the alternative to these taggable?() checks is to explicitly include a + # TaggingMethods module (which you would create) in each taggable model + + def tag_with list + # completely replace the existing tag set + taggable?(true) + list = tag_cast_to_string(list) + + Tag.transaction do # transactions may not be ideal for you here + current = <%= parent_association_name -%>s.map(&:name) + _add_tags(list - current) + _remove_tags(current - list) + end + + self + end + + alias :<%= parent_association_name -%>s= :tag_with + + # need to avoid name conflicts with the built-in ActiveRecord association + # methods, thus the underscores + def _add_tags incoming + taggable?(true) + tag_cast_to_string(incoming).each do |tag_name| + begin + tag = Tag.find_or_create_by_name(tag_name) + raise Tag::Error, "tag could not be saved: #{tag_name}" if tag.new_record? + tag.taggables << self + rescue ActiveRecord::StatementInvalid => e + raise unless e.to_s =~ /duplicate/i + end + end + end + + def _remove_tags outgoing + taggable?(true) + outgoing = tag_cast_to_string(outgoing) +<% if options[:self_referential] %> + # because of http://dev.rubyonrails.org/ticket/6466 + taggings.destroy(taggings.find(:all, :include => :<%= parent_association_name -%>).select do |tagging| + outgoing.include? tagging.<%= parent_association_name -%>.name + end) +<% else -%> + <%= parent_association_name -%>s.delete(<%= parent_association_name -%>s.select do |tag| + outgoing.include? tag.name + end) +<% end -%> + end + + def tag_list + taggable?(true) + <%= parent_association_name -%>s.reload + <%= parent_association_name -%>s.to_s + end + + private + + def tag_cast_to_string obj + case obj + when Array + obj.map! do |item| + case item + when /^\d+$/, Fixnum then Tag.find(item).name # this will be slow if you use ids a lot + when Tag then item.name + when String then item + else + raise "Invalid type" + end + end + when String + obj = obj.split(Tag::DELIMITER).map do |tag_name| + tag_name.strip.squeeze(" ") + end + else + raise "Invalid object of class #{obj.class} as tagging method parameter" + end.flatten.compact.map(&:downcase).uniq + end + + def taggable?(should_raise = false) + unless flag = respond_to?(:<%= parent_association_name -%>s) + raise "#{self.class} is not a taggable model" if should_raise + end + flag + end + +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging_test.rb b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging_test.rb new file mode 100644 index 00000000..ff5eb2d9 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging_test.rb @@ -0,0 +1,58 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class TaggingTest < Test::Unit::TestCase + fixtures :taggings, :tags, :posts, :recipes + + def setup + @obj1 = Recipe.find(1) + @obj2 = Recipe.find(2) + @obj3 = Post.find(1) + @tag1 = Tag.find(1) + @tag2 = Tag.find(2) + @tagging1 = Tagging.find(1) + end + + def test_tag_with + @obj2.tag_with "dark columbian" + assert_equal "columbian dark", @obj2.tag_list + end + +<% if options[:self_referential] -%> + def test_self_referential_tag_with + @tag1.tag_with [1, 2] + assert @tag1.tags.include?(@tag1) + assert !@tag2.tags.include?(@tag1) + end + +<% end -%> + def test__add_tags + @obj1._add_tags "porter longneck" + assert Tag.find_by_name("porter").taggables.include?(@obj1) + assert Tag.find_by_name("longneck").taggables.include?(@obj1) + assert_equal "delicious longneck porter", @obj1.tag_list + + @obj1._add_tags [2] + assert_equal "delicious longneck porter sexy", @obj1.tag_list + end + + def test__remove_tags + @obj2._remove_tags ["2", @tag1] + assert @obj2.tags.empty? + end + + def test_tag_list + assert_equal "delicious sexy", @obj2.tag_list + end + + def test_taggable + assert_raises(RuntimeError) do + @tagging1.send(:taggable?, true) + end + assert !@tagging1.send(:taggable?) + assert @obj3.send(:taggable?) +<% if options[:self_referential] -%> + assert @tag1.send(:taggable?) +<% end -%> + end + +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/taggings.yml b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/taggings.yml new file mode 100644 index 00000000..5da04182 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/taggings.yml @@ -0,0 +1,21 @@ +--- +taggings_003: + <%= parent_association_name -%>_id: "2" + id: "3" + taggable_type: <%= taggable_models[0][1..-1].classify %> + taggable_id: "1" +taggings_004: + <%= parent_association_name -%>_id: "2" + id: "4" + taggable_type: <%= taggable_models[1][1..-1].classify %> + taggable_id: "2" +taggings_001: + <%= parent_association_name -%>_id: "1" + id: "1" + taggable_type: <%= taggable_models[1][1..-1].classify %> + taggable_id: "1" +taggings_002: + <%= parent_association_name -%>_id: "1" + id: "2" + taggable_type: <%= taggable_models[1][1..-1].classify %> + taggable_id: "2" diff --git a/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tags.yml b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tags.yml new file mode 100644 index 00000000..3bf1078d --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tags.yml @@ -0,0 +1,7 @@ +--- +tags_001: + name: delicious + id: "1" +tags_002: + name: sexy + id: "2" diff --git a/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tags_controller.rb b/tracks/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tags_controller.rb new file mode 100644 index 00000000..e69de29b diff --git a/tracks/vendor/plugins/has_many_polymorphs/init.rb b/tracks/vendor/plugins/has_many_polymorphs/init.rb index 55b471e4..3939a253 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/init.rb +++ b/tracks/vendor/plugins/has_many_polymorphs/init.rb @@ -1 +1,2 @@ + require 'has_many_polymorphs' diff --git a/tracks/vendor/plugins/has_many_polymorphs/install.rb b/tracks/vendor/plugins/has_many_polymorphs/install.rb new file mode 100644 index 00000000..2261ffa0 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/install.rb @@ -0,0 +1 @@ +puts open("#{File.dirname(__FILE__)}/TAGGING_INSTALL").read diff --git a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs.rb b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs.rb index b11502cc..a3601aec 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs.rb +++ b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs.rb @@ -1,581 +1,31 @@ # self-referential, polymorphic has_many :through plugin -# http://blog.evanweaver.com/articles/2006/06/02/has_many_polymorphs +# http://blog.evanweaver.com/pages/has_many_polymorphs # operates via magic dust, and courage -if defined? Rails::Configuration - class Rails::Configuration - def has_many_polymorphs_cache_classes= *args - ::ActiveRecord::Associations::ClassMethods.has_many_polymorphs_cache_classes = *args - end - end +require 'active_record' + +require 'has_many_polymorphs/reflection' +require 'has_many_polymorphs/association' +require 'has_many_polymorphs/class_methods' + +require 'has_many_polymorphs/support_methods' +require 'has_many_polymorphs/configuration' +require 'has_many_polymorphs/base' + +class ActiveRecord::Base + extend ActiveRecord::Associations::PolymorphicClassMethods end -module ActiveRecord - - if ENV['RAILS_ENV'] =~ /development|test/ and ENV['USER'] == 'eweaver' - # enable this condition to get awesome association debugging - # you will get a folder "generated_models" in the current dir containing valid Ruby files - # explaining all ActiveRecord relationships set up by the plugin, as well as listing the - # line in the plugin that made each particular macro call - class << Base - COLLECTION_METHODS = [:belongs_to, :has_many, :has_and_belongs_to_many, :has_one].each do |method_name| - alias_method "original_#{method_name}".to_sym, method_name - undef_method method_name - end - - unless defined? GENERATED_CODE_DIR - # automatic code generation for debugging... bitches - GENERATED_CODE_DIR = "generated_models" - system "rm -rf #{GENERATED_CODE_DIR}" - Dir.mkdir GENERATED_CODE_DIR - - alias :original_method_missing :method_missing - def method_missing(method_name, *args, &block) - if COLLECTION_METHODS.include? method_name.to_sym - Dir.chdir GENERATED_CODE_DIR do - filename = "#{ActiveRecord::Associations::ClassMethods.demodulate(self.name.underscore)}.rb" - contents = File.open(filename).read rescue "\nclass #{self.name}\n\nend\n" - line = caller[1][/\:(\d+)\:/, 1] - contents[-5..-5] = "\n #{method_name} #{args[0..-2].inspect[1..-2]},\n #{args[-1].inspect[1..-2].gsub(" :", "\n :").gsub("=>", " => ")}\n#{ block ? " #{block.inspect.sub(/\@.*\//, '@')}\n" : ""} # called from line #{line}\n\n" - File.open(filename, "w") do |file| - file.puts contents - end - end - # doesn't handle blocks - self.send("original_#{method_name}", *args, &block) - else - self.send(:original_method_missing, method_name, *args, &block) - end - end - end - end - - # and we want to track the reloader's shenanigans - (::Dependencies.log_activity = true) rescue nil - end - - module Associations - module ClassMethods - mattr_accessor :has_many_polymorphs_cache_classes - - def acts_as_double_polymorphic_join opts - raise RuntimeError, "Couldn't understand #{opts.inspect} options in acts_as_double_polymorphic_join. Please only specify the two relationships and their member classes; there are no options to set. " unless opts.length == 2 - - join_name = self.name.tableize.to_sym - opts.each do |polymorphs, children| - parent_hash_key = (opts.keys - [polymorphs]).first # parents are the entries in the _other_ children array - - begin - parent_foreign_key = self.reflect_on_association(parent_hash_key.to_s.singularize.to_sym).primary_key_name - rescue NoMethodError - raise RuntimeError, "Couldn't find 'belongs_to' association for :#{parent_hash_key.to_s.singularize} in #{self.name}." unless parent_foreign_key - end - - parents = opts[parent_hash_key] - conflicts = (children & parents) # set intersection - parents.each do |parent_name| - - parent_class = parent_name.to_s.classify.constantize - reverse_polymorph = parent_hash_key.to_s.singularize - polymorph = polymorphs.to_s.singularize - - parent_class.send(:has_many_polymorphs, - polymorphs, {:double => true, - :from => children, - :as => parent_hash_key.to_s.singularize.to_sym, - :through => join_name, - :dependent => :destroy, - :foreign_key => parent_foreign_key, - :foreign_type_key => parent_foreign_key.to_s.sub(/_id$/, '_type'), - :reverse_polymorph => reverse_polymorph, - :conflicts => conflicts, - :rename_individual_collections => false}) - - if conflicts.include? parent_name - # unify the alternate sides of the conflicting children - (conflicts).each do |method_name| - unless parent_class.instance_methods.include?(method_name) - parent_class.send(:define_method, method_name) do - (self.send("#{reverse_polymorph}_#{method_name}") + - self.send("#{polymorph}_#{method_name}")).freeze - end - end - end - - # unify the join model - unless parent_class.instance_methods.include?(join_name) - parent_class.send(:define_method, join_name) do - (self.send("#{join_name}_as_#{reverse_polymorph}") + - self.send("#{join_name}_as_#{polymorph}")).freeze - end - end - - end - end - end - end - - def has_many_polymorphs(polymorphs, options, &block) - options.assert_valid_keys(:from, :acts_as, :as, :through, :foreign_key, :dependent, :double, - :rename_individual_collections, :foreign_type_key, :reverse_polymorph, :conflicts) - - # the way this deals with extra parameters to the associations could use some work - options[:as] ||= options[:acts_as] ||= self.table_name.singularize.to_sym - - # foreign keys follow the table name, not the class name in Rails 2.0 - options[:foreign_key] ||= "#{options[:as].to_s}_id" - - # no conflicts by default - options[:conflicts] ||= [] - - # construct the join table name - options[:through] ||= join_table((options[:as].to_s.pluralize or self.table_name), polymorphs) - if options[:reverse_polymorph] - options[:through_with_reverse_polymorph] = "#{options[:through]}_as_#{options[:reverse_polymorph]}".to_sym - else - options[:through_with_reverse_polymorph] = options[:through] - end - - options[:join_class_name] ||= options[:through].to_s.classify - - # the class must have_many on the join_table - opts = {:foreign_key => options[:foreign_key], :dependent => options[:dependent], - :class_name => options[:join_class_name]} - if options[:foreign_type_key] - opts[:conditions] = "#{options[:foreign_type_key]} = #{quote_value self.base_class.name}" - end - - has_many demodulate(options[:through_with_reverse_polymorph]), opts - - polymorph = polymorphs.to_s.singularize.to_sym - - # add the base_class method to the join_table so that STI will work transparently - inject_before_save_into_join_table(options[:join_class_name], polymorph) - - # get some reusable info - children, child_associations = {}, {} - options[:from].each do |child_plural| - children[child_plural] = child_plural.to_s.singularize.to_sym - child_associations[child_plural] = (options[:rename_individual_collections] ? "#{polymorph}_#{child_plural}".to_sym : child_plural) - end - - # get our models out of the reloadable lists, if requested - if self.has_many_polymorphs_cache_classes - klasses = [self.name, options[:join_class_name], *children.values.map{|x| x.to_s.classify}] - klasses += basify_sti_classnames(klasses).keys.to_a.compact.uniq.map{|x| x.to_s.classify} - klasses.uniq! - klasses.each {|s| logger.debug "Ejecting #{s.inspect} from the autoload lists"} - begin - Dependencies.autoloaded_constants -= klasses - Dependencies.explicitly_unloadable_constants -= klasses - rescue NoMethodError - raise "Rails 1.2.0 or later is required to set config.has_many_polymorphs_cache_classes = true" - end - end - - # auto-inject individually named associations for the children into the join model - create_virtual_associations_for_join_to_individual_children(children, polymorph, options) - - # iterate through the polymorphic children, running the parent class's :has_many on each one - create_has_many_through_associations_for_parent_to_children(children, child_associations, polymorphs, polymorph, options) - - # auto-inject the regular polymorphic associations into the child classes - create_has_many_through_associations_for_children_to_parent(children, polymorph, options) - - create_general_collection_association_for_parent(polymorphs, polymorph, basify_sti_classnames(children), options, &block) - end - - def self.demodulate(s) - s.to_s.gsub('/', '_').to_sym - end - - protected - - def demodulate(s) - ActiveRecord::Associations::ClassMethods.demodulate(s) - end - - def basify_sti_classnames(hash) - # this blows - result = {} - hash.each do |plural, singular| - klass = plural.to_s.classify.constantize - if klass != klass.base_class - result[klass.base_class.table_name.to_sym] = klass.base_class.table_name.singularize.to_sym - else - result[plural] = singular - end - end - result - end - - def inject_before_save_into_join_table(join_class_name, polymorph) - sti_hook = "sti_class_rewrite" - rewrite_procedure = %[ - self.send(:#{polymorph}_type=, self.#{polymorph}_type.constantize.base_class.name) - ] - - # this also blows, and should be abstracted. alias_method_chain is not enough. - join_class_name.constantize.class_eval %[ - unless instance_methods.include? "before_save_with_#{sti_hook}" - if instance_methods.include? "before_save" - alias_method :before_save_without_#{sti_hook}, :before_save - def before_save_with_#{sti_hook} - before_save_without_#{sti_hook} - #{rewrite_procedure} - end - else - def before_save_with_#{sti_hook} - #{rewrite_procedure} - end - end - alias_method :before_save, :before_save_with_#{sti_hook} - end - ] - - end - - def create_virtual_associations_for_join_to_individual_children(children, polymorph, options) - children.each do |child_plural, child| - options[:join_class_name].constantize.instance_eval do - - association_name = child.to_s - association_name += "_as_#{polymorph}" if options[:conflicts].include?(child_plural) - association = demodulate(association_name) - - opts = {:class_name => child.to_s.classify, - :foreign_key => "#{polymorph}_id" } - - unless self.reflect_on_all_associations.map(&:name).include? association - belongs_to association, opts - end - - end - end - end - - def create_has_many_through_associations_for_children_to_parent(children, polymorph, options) - children.each do |child_plural, child| - - if child == options[:as] - raise RuntimeError, "You can't have a self-referential polymorphic has_many :through without renaming the non-polymorphic foreign key in the join model." - end - - parent = self - child.to_s.classify.constantize.instance_eval do - - # this shouldn't be called at all during doubles; there is no way to traverse to a - # double polymorphic parent (XXX is that right?) - unless options[:double] or options[:conflicts].include? self.name.tableize.to_sym - begin - require_dependency parent.name.underscore # XXX why is this here? - rescue MissingSourceFile - end - - # the join table - through = demodulate(options[:through_with_reverse_polymorph]).to_s - through += "_as_child" if parent == self - through = through.to_sym - - has_many through, :as => polymorph, - :class_name => options[:through].to_s.classify, - :dependent => options[:dependent] - - association = options[:as].to_s.pluralize - association += "_of_#{polymorph.to_s.pluralize}" if options[:rename_individual_collections] # XXX check this - - # the polymorphic parent association - has_many association.to_sym, :through => through, - :class_name => parent.name, - :source => options[:as], - :foreign_key => options[:foreign_key] - end - - end - end - end - - def create_has_many_through_associations_for_parent_to_children(children, child_associations, polymorphs, polymorph, options) - children.each do |child_plural, child| - #puts ":source => #{child}" - association = demodulate(child_associations[child_plural]).to_s - source = demodulate(child).to_s - - if options[:conflicts].include? child_plural - # XXX what? - association = "#{polymorph}_#{association}" if options[:conflicts].include? self.name.tableize.to_sym - source += "_as_#{polymorph}" - end - - # activerecord is broken when you try to anonymously extend an association in a namespaced model, - extension = self.class_eval %[ - module #{association.classify + "AssociationExtension"} - def push *args - proxy_owner.send(:#{polymorphs}).send(:push, *args).select{|x| x.is_a? #{child.to_s.classify}} - end - alias :<< :push - def delete *args - proxy_owner.send(:#{polymorphs}).send(:delete, *args) - end - def clear - proxy_owner.send(:#{polymorphs}).send(:clear, #{child.to_s.classify}) - end - self # required - end] - - has_many association.to_sym, :through => demodulate(options[:through_with_reverse_polymorph]), - :source => source.to_sym, - :conditions => ["#{options[:join_class_name].constantize.table_name}.#{polymorph}_type = ?", child.to_s.classify.constantize.base_class.name], - :extend => extension - - end - end - - def create_general_collection_association_for_parent(collection_name, polymorph, children, options, &block) - # we need to explicitly rename all the columns because we are fetching all the children objects at once. - # if multiple objects have a 'title' column, for instance, there will be a collision and we will potentially - # lose data. if we alias the fields and then break them up later, there are no collisions. - join_model = options[:through].to_s.classify.constantize - - # figure out what fields we wanna grab - select_fields = [] - children.each do |plural, singular| - klass = plural.to_s.classify.constantize - klass.columns.map(&:name).each do |name| - select_fields << "#{klass.table_name}.#{name} as #{demodulate plural}_#{name}" - end - end - - # now get the join model fields - join_model.columns.map(&:name).each do |name| - select_fields << "#{join_model.table_name}.#{name} as #{join_model.table_name}_#{name}" - end - - from_table = self.table_name - left_joins = children.keys.map do |n| - klass = n.to_s.classify.constantize - "LEFT JOIN #{klass.table_name} ON #{join_model.table_name}.#{polymorph}_id = #{klass.table_name}.#{klass.primary_key} AND #{join_model.table_name}.#{polymorph}_type = '#{n.to_s.classify}'" - end - - sql_query = 'SELECT ' + select_fields.join(', ') + " FROM #{join_model.table_name}" + - "\nJOIN #{from_table} as polymorphic_parent ON #{join_model.table_name}.#{options[:foreign_key]} = polymorphic_parent.#{self.primary_key}\n" + - left_joins.join("\n") + "\nWHERE " - - if options[:foreign_type_key] - sql_query +="#{join_model.table_name}.#{options[:foreign_type_key]} = #{quote_value self.base_class.name} AND " - end - - # for sqlite3 you have to reference the left-most table in WHERE clauses or rows with NULL - # join results sometimes get silently dropped. it's stupid. - sql_query += "#{join_model.table_name}.#{options[:foreign_key]} " - #puts("Built collection property query:\n #{sql_query}") - - class_eval do - attr_accessor "#{collection_name}_cache" - cattr_accessor "#{collection_name}_options" - - define_method(collection_name) do - if collection_name_cache = instance_variable_get("@#{collection_name}_cache") - #puts("Cache hit on #{collection_name}") - collection_name_cache - else - #puts("Cache miss on #{collection_name}") - rows = connection.select_all("#{sql_query}" + (new_record? ? "IS NULL" : "= #{self.id}")) - # this gives us a hash with keys for each object type - objectified = objectify_polymorphic_array(rows, "#{join_model}", "#{polymorph}_type") - # locally cache the different object types found - # this doesn't work... yet. - objectified.each do |key, array| - instance_variable_set("@#{ActiveRecord::Associations::ClassMethods.demodulate(key)}", array) - end - proxy_object = HasManyPolymorphsProxyCollection.new(objectified[:all], self, send("#{collection_name}_options")) - (class << proxy_object; self end).send(:class_eval, &block) if block_given? - instance_variable_set("@#{collection_name}_cache", proxy_object) - end - end - - # in order not to break tests, see if we have been defined already - unless instance_methods.include? "reload_with_#{collection_name}" - define_method("reload_with_#{collection_name}") do - send("reload_without_#{collection_name}") - instance_variable_set("@#{collection_name}_cache", nil) - self - end - - alias_method "reload_without_#{collection_name}", :reload - alias_method :reload, "reload_with_#{collection_name}" - end - end - - send("#{collection_name}_options=", - options.merge(:collection_name => collection_name, - :type_key => "#{polymorph}_type", - :id_key => "#{polymorph}_id")) - -# puts("Defined the collection proxy.\n#{collection_name}\n") - end - - def join_table(a, b) - [a.to_s, b.to_s].sort.join("_").to_sym - end - - unless self.respond_to? :quote_value - # hack it in (very badly) for Rails 1.1.6 people - def quote_value s - "'#{s.inspect[1..-2]}'" - end - end - - end - - ################################################ - - # decided to leave this alone unless it becomes clear that there is some benefit - # in deriving from AssociationProxy - # - # the benefit would be custom finders on the collection, perhaps... - class HasManyPolymorphsProxyCollection < Array - - alias :array_delete :delete - alias :array_push :push - alias :count :length - - def initialize(contents, parent, options) - @parent = parent - @options = options - @join_class = options[:join_class_name].constantize - return if contents.blank? - super(contents) - end - - def push(objs, args={}) - objs = [objs] unless objs.is_a? Array - - objs.each do |obj| - data = {@options[:foreign_key] => @parent.id, - @options[:type_key] => obj.class.base_class.to_s, @options[:id_key] => obj.id} - data.merge!({@options[:foreign_type_key] => @parent.class.base_class.to_s}) if @options[:foreign_type_key] # for double polymorphs - conditions_string = data.keys.map(&:to_s).push("").join(" = ? AND ")[0..-6] - if @join_class.find(:first, :conditions => [conditions_string] + data.values).blank? - @join_class.new(data).save! - end - end - - if args[:reload] - reload - else - # we have to do this funky stuff instead of just array difference because +/.uniq returns a regular array, - # which doesn't have our special methods and configuration anymore - unless (difference = objs - collection).blank? - @parent.send("#{@options[:collection_name]}_cache=".to_sym, collection.array_push(*difference)) - end - end - - @parent.send(@options[:collection_name]) - end - - alias :<< :push - - def delete(objs, args={}) - - if objs - objs = [objs] unless objs.is_a? Array - elsif args[:clear] - objs = collection - objs = objs.select{|obj| obj.is_a? args[:klass]} if args[:klass] - else - raise RuntimeError, "Invalid delete parameters (has_many_polymorphs)." - end - - records = [] - objs.each do |obj| - records += join_records.select do |record| - record.send(@options[:type_key]) == obj.class.base_class.to_s and - record.send(@options[:id_key]) == obj.id - end - end - - reload if args[:reload] - unless records.blank? - records.map(&:destroy) - # XXX could be faster if we reversed the loops - deleted_items = collection.select do |item| - records.select {|join_record| - join_record.send(@options[:type_key]) == item.class.base_class.name and - join_record.send(@options[:id_key]) == item.id - }.length > 0 - end - # keep the cache fresh, while we're at it. see comment in .push - deleted_items.each { |item| collection.array_delete(item) } - @parent.send("#{@options[:collection_name]}_cache=", collection) - - return deleted_items unless deleted_items.empty? - end - nil - end - - def clear(klass = nil) - result = delete(nil, :clear => true, :klass => klass) - return result if result - collection - end - - def reload - # reset the cache, postponing reloading from the db until we really need it - @parent.reload - end - - private - def join_records - @parent.send(ActiveRecord::Associations::ClassMethods.demodulate(@options[:through])) - end - - def collection - @parent.send(@options[:collection_name]) - end - - end - end - - - class Base - # turns an array of hashes (db rows) into a hash consisting of :all (array of everything) and - # a hash key for each class type it finds, e.g. :posts and :comments - private - def objectify_polymorphic_array(array, join_model, type_field) - join_model = join_model.constantize - arrays_hash = {} - - array.each do |element| - klass = element["#{join_model.table_name}_#{type_field}"].constantize - association = ActiveRecord::Associations::ClassMethods.demodulate(klass.name.pluralize.underscore.downcase) - hash = {} - -# puts "Class #{klass.inspect}" -# puts "Association name: #{association.inspect}" - - element.each do |key, value| -# puts "key #{key} - value #{value.inspect}" - if key =~ /^#{association}_(.+)/ - hash[$1] = value -# puts "#{$1.inspect} assigned #{value.inspect}" - end - end - - object = klass.instantiate(hash) - - arrays_hash[:all] ||= [] - arrays_hash[association] ||= [] - arrays_hash[:all] << object - arrays_hash[association] << object - end - - arrays_hash - end - end +if ENV['RAILS_ENV'] =~ /development|test/ and ENV['USER'] == 'eweaver' + _logger_warn "has_many_polymorphs: debug mode enabled" + require 'has_many_polymorphs/debugging_tools' end -#require 'ruby-debug' -#Debugger.start +if defined? Rails and RAILS_ENV and RAILS_ROOT + _logger_warn "has_many_polymorphs: Rails environment detected" + require 'has_many_polymorphs/dependencies' + require 'has_many_polymorphs/autoload' +end +_logger_debug "has_many_polymorphs: loaded ok" diff --git a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/association.rb b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/association.rb new file mode 100644 index 00000000..f2849ad9 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/association.rb @@ -0,0 +1,153 @@ +module ActiveRecord + module Associations + + class PolymorphicError < ActiveRecordError; end + class PolymorphicMethodNotSupportedError < ActiveRecordError; end + + class PolymorphicAssociation < HasManyThroughAssociation + + def <<(*records) + return if records.empty? + + if @reflection.options[:skip_duplicates] + _logger_debug "Loading instances for polymorphic duplicate push check; use :skip_duplicates => false and perhaps a database constraint to avoid this possible performance issue" + load_target + end + + @reflection.klass.transaction do + flatten_deeper(records).each do |record| + if @owner.new_record? or not record.respond_to?(:new_record?) or record.new_record? + raise PolymorphicError, "You can't associate unsaved records." + end + next if @reflection.options[:skip_duplicates] and @target.include? record + @owner.send(@reflection.through_reflection.name).proxy_target << @reflection.klass.create!(construct_join_attributes(record)) + @target << record if loaded? + end + end + + self + end + + alias :push :<< + alias :concat :<< + + def find(*args) + # super(*(args << returning(Base.send(:extract_options_from_args!, args)) {|opts| opts.delete :include})) # returning is slow + opts = Base.send(:extract_options_from_args!, args) + opts.delete :include + super(*(args + [opts])) + end + + def construct_scope + _logger_warn "Warning; not all usage scenarios for polymorphic scopes are supported yet." + super + end + + def delete(*records) + records = flatten_deeper(records) + records.reject! {|record| @target.delete(record) if record.new_record?} + return if records.empty? + + @reflection.klass.transaction do + records.each do |record| + joins = @reflection.through_reflection.name + @owner.send(joins).delete(@owner.send(joins).select do |join| + join.send(@reflection.options[:polymorphic_key]) == record.id and + join.send(@reflection.options[:polymorphic_type_key]) == "#{record.class.base_class}" + end) + @target.delete(record) + end + end +# records + end + + def clear(klass = nil) + load_target + return if @target.empty? + + if klass + delete(@target.select {|r| r.is_a? klass }) + else + @owner.send(@reflection.through_reflection.name).clear + @target.clear + end + [] + end + +# undef :sum +# undef :create! + + protected + + def construct_quoted_owner_attributes(*args) + # no access to returning() here? why not? + type_key = @reflection.options[:foreign_type_key] + {@reflection.primary_key_name => @owner.id, + type_key=> (@owner.class.base_class.name if type_key)} + end + + def construct_from + # build the FROM part of the query, in this case, the polymorphic join table + @reflection.klass.table_name + end + + def construct_owner + # the table name for the owner object's class + @owner.class.table_name + end + + def construct_owner_key + # the primary key field for the owner object + @owner.class.primary_key + end + + def construct_select(custom_select = nil) + # build the select query + selected = custom_select || @reflection.options[:select] + end + + def construct_joins(custom_joins = nil) + # build the string of default joins + "JOIN #{construct_owner} AS polymorphic_parent ON #{construct_from}.#{@reflection.options[:foreign_key]} = polymorphic_parent.#{construct_owner_key} " + + @reflection.options[:from].map do |plural| + klass = plural._as_class + "LEFT JOIN #{klass.table_name} ON #{construct_from}.#{@reflection.options[:polymorphic_key]} = #{klass.table_name}.#{klass.primary_key} AND #{construct_from}.#{@reflection.options[:polymorphic_type_key]} = #{@reflection.klass.quote_value(klass.base_class.name)}" + end.uniq.join(" ") + " #{custom_joins}" + end + + def construct_conditions + # build the fully realized condition string + conditions = construct_quoted_owner_attributes.map do |field, value| + "#{construct_from}.#{field} = #{@reflection.klass.quote_value(value)}" if value + end + conditions << custom_conditions if custom_conditions + "(" + conditions.compact.join(') AND (') + ")" + end + + def custom_conditions + # custom conditions... not as messy as has_many :through because our joins are a little smarter + if @reflection.options[:conditions] + "(" + interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) + ")" + end + end + + alias :construct_owner_attributes :construct_quoted_owner_attributes + alias :conditions :custom_conditions # XXX possibly not necessary + alias :sql_conditions :custom_conditions # XXX ditto + + # construct attributes for join for a particular record + def construct_join_attributes(record) + {@reflection.options[:polymorphic_key] => record.id, + @reflection.options[:polymorphic_type_key] => "#{record.class.base_class}", + @reflection.options[:foreign_key] => @owner.id}.merge(@reflection.options[:foreign_type_key] ? + {@reflection.options[:foreign_type_key] => "#{@owner.class.base_class}"} : {}) # for double-sided relationships + end + + def build(attrs = nil) + raise PolymorphicMethodNotSupportedError, "You can't associate new records." + end + + end + + end +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/autoload.rb b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/autoload.rb new file mode 100644 index 00000000..e2cfc1ed --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/autoload.rb @@ -0,0 +1,26 @@ + +require 'initializer' + +class Rails::Initializer + def after_initialize_with_autoload + after_initialize_without_autoload + + _logger_debug "has_many_polymorphs: autoload hook invoked" + Dir["#{RAILS_ROOT}/app/models/**/*.rb"].each do |filename| + next if filename =~ /svn|CVS|bzr/ + open filename do |file| + if file.grep(/has_many_polymorphs|acts_as_double_polymorphic_join/).any? + begin + model = File.basename(filename)[0..-4].classify + model.constantize + _logger_warn "has_many_polymorphs: preloaded parent model #{model}" + rescue Object => e + _logger_warn "error preloading #{model}: #{e.inspect}" + end + end + end + end + end + + alias_method_chain :after_initialize, :autoload +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/base.rb b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/base.rb new file mode 100644 index 00000000..eeb44217 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/base.rb @@ -0,0 +1,47 @@ + +module ActiveRecord + class Base + + class << self + def instantiate_without_callbacks_with_polymorphic_checks(record) + if record['polymorphic_parent_class'] + reflection = record['polymorphic_parent_class'].constantize.reflect_on_association(record['polymorphic_association_id'].to_sym) +# _logger_debug "Instantiating a polymorphic row for #{record['polymorphic_parent_class']}.reflect_on_association(:#{record['polymorphic_association_id']})" + + # rewrite 'record' with the right column names + table_aliases = reflection.options[:table_aliases].dup + record = Hash[*table_aliases.keys.map {|key| [key, record[table_aliases[key]]] }.flatten] + + # find the real child class + klass = record["#{self.table_name}.#{reflection.options[:polymorphic_type_key]}"].constantize + if sti_klass = record["#{klass.table_name}.#{klass.inheritance_column}"] + klass = klass.class_eval do compute_type(sti_klass) end # in case of namespaced STI models + end + + # check that the join actually joined to something + unless (child_id = record["#{self.table_name}.#{reflection.options[:polymorphic_key]}"]) == record["#{klass.table_name}.#{klass.primary_key}"] + raise ActiveRecord::Associations::PolymorphicError, + "Referential integrity violation; child <#{klass.name}:#{child_id}> was not found for #{reflection.name.inspect}" + end + + # eject the join keys + record = Hash[*record._select do |column, value| + column[/^#{klass.table_name}/] + end.map do |column, value| + [column[/\.(.*)/, 1], value] + end.flatten] + + # allocate and assign values + returning(klass.allocate) do |obj| + obj.instance_variable_set("@attributes", record) + end + else + instantiate_without_callbacks_without_polymorphic_checks(record) + end + end + + alias_method_chain :instantiate_without_callbacks, :polymorphic_checks # oh yeah + end + + end +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/class_methods.rb b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/class_methods.rb new file mode 100644 index 00000000..baae7fdd --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/class_methods.rb @@ -0,0 +1,452 @@ + +module ActiveRecord::Associations + module PolymorphicClassMethods + + ################# + # AR::Base association macros + + RESERVED_KEYS = [:conditions, :order, :limit, :offset, :extend, :skip_duplicates, + :join_extend, :dependent, :rename_individual_collections] + + def acts_as_double_polymorphic_join options={}, &extension + + collections = options._select {|k,v| v.is_a? Array and k.to_s !~ /(#{RESERVED_KEYS.map(&:to_s).join('|')})$/} + raise PolymorphicError, "Couldn't understand options in acts_as_double_polymorphic_join. Valid parameters are your two class collections, and then #{RESERVED_KEYS.inspect[1..-2]}, with optionally your collection names prepended and joined with an underscore." unless collections.size == 2 + + options = options._select {|k,v| !collections[k]} + options[:extend] = (options[:extend] ? Array(options[:extend]) + [extension] : extension) if extension # inline the block + + collection_option_keys = Hash[*collections.keys.map do |key| + [key, RESERVED_KEYS.map{|option| "#{key}_#{option}".to_sym}] + end._flatten_once] + + collections.keys.each do |collection| + options.each do |key, value| + next if collection_option_keys.values.flatten.include? key + # shift the general options to the individual sides + collection_value = options[collection_key = "#{collection}_#{key}".to_sym] + case key + when :conditions + collection_value, value = sanitize_sql(collection_value), sanitize_sql(value) + options[collection_key] = (collection_value ? "(#{collection_value}) AND (#{value})" : value) + when :order + options[collection_key] = (collection_value ? "#{collection_value}, #{value}" : value) + when :extend, :join_extend + options[collection_key] = Array(collection_value) + Array(value) + when :limit, :offset, :dependent, :rename_individual_collections + options[collection_key] ||= value + else + raise PolymorphicError, "Unknown option key #{key.inspect}." + end + end + end + + join_name = self.name.tableize.to_sym + collections.each do |association_id, children| + parent_hash_key = (collections.keys - [association_id]).first # parents are the entries in the _other_ children array + + begin + parent_foreign_key = self.reflect_on_association(parent_hash_key._singularize).primary_key_name + rescue NoMethodError + raise PolymorphicError, "Couldn't find 'belongs_to' association for :#{parent_hash_key._singularize} in #{self.name}." unless parent_foreign_key + end + + parents = collections[parent_hash_key] + conflicts = (children & parents) # set intersection + parents.each do |plural_parent_name| + + parent_class = plural_parent_name._as_class + singular_reverse_association_id = parent_hash_key._singularize + + parent_class.send(:has_many_polymorphs, + association_id, {:is_double => true, + :from => children, + :as => singular_reverse_association_id, + :through => join_name.to_sym, + :foreign_key => parent_foreign_key, + :foreign_type_key => parent_foreign_key.to_s.sub(/_id$/, '_type'), + :singular_reverse_association_id => singular_reverse_association_id, + :conflicts => conflicts}.merge(Hash[*options._select do |key, value| + collection_option_keys[association_id].include? key and !value.nil? + end.map do |key, value| + [key.to_s[association_id.to_s.length+1..-1].to_sym, value] + end._flatten_once])) # rename side-specific options to general names + + if conflicts.include? plural_parent_name + # unify the alternate sides of the conflicting children + (conflicts).each do |method_name| + unless parent_class.instance_methods.include?(method_name) + parent_class.send(:define_method, method_name) do + (self.send("#{singular_reverse_association_id}_#{method_name}") + + self.send("#{association_id._singularize}_#{method_name}")).freeze + end + end + end + + # unify the join model... join model is always renamed for doubles, unlike child associations + unless parent_class.instance_methods.include?(join_name) + parent_class.send(:define_method, join_name) do + (self.send("#{join_name}_as_#{singular_reverse_association_id}") + + self.send("#{join_name}_as_#{association_id._singularize}")).freeze + end + end + else + unless parent_class.instance_methods.include?(join_name) + parent_class.send(:alias_method, join_name, "#{join_name}_as_#{singular_reverse_association_id}") + end + end + + end + end + end + + def has_many_polymorphs (association_id, options = {}, &extension) + _logger_debug "has_many_polymorphs: INIT" + reflection = create_has_many_polymorphs_reflection(association_id, options, &extension) + # puts "Created reflection #{reflection.inspect}" + # configure_dependency_for_has_many(reflection) + collection_reader_method(reflection, PolymorphicAssociation) + end + + def create_has_many_polymorphs_reflection(association_id, options, &extension) + options.assert_valid_keys( + :from, + :as, + :through, + :foreign_key, + :foreign_type_key, + :polymorphic_key, # same as :association_foreign_key + :polymorphic_type_key, + :dependent, # default :destroy, only affects the join table + :skip_duplicates, # default true, only affects the polymorphic collection + :ignore_duplicates, # deprecated + :is_double, + :rename_individual_collections, + :reverse_association_id, # not used + :singular_reverse_association_id, + :conflicts, + :extend, + :join_class_name, + :join_extend, + :parent_extend, + :table_aliases, + :select, # applies to the polymorphic relationship + :conditions, # applies to the polymorphic relationship, the children, and the join +# :include, + :order, # applies to the polymorphic relationship, the children, and the join + :group, # only applies to the polymorphic relationship and the children + :limit, # only applies to the polymorphic relationship and the children + :offset, # only applies to the polymorphic relationship + :parent_order, + :parent_group, + :parent_limit, + :parent_offset, +# :source, + :uniq, # XXX untested, only applies to the polymorphic relationship +# :finder_sql, +# :counter_sql, +# :before_add, +# :after_add, +# :before_remove, +# :after_remove + :dummy) + + # validate against the most frequent configuration mistakes + verify_pluralization_of(association_id) + raise PolymorphicError, ":from option must be an array" unless options[:from].is_a? Array + options[:from].each{|plural| verify_pluralization_of(plural)} + + options[:as] ||= self.table_name.singularize.to_sym + options[:conflicts] = Array(options[:conflicts]) + options[:foreign_key] ||= "#{options[:as]}_id" + + options[:association_foreign_key] = + options[:polymorphic_key] ||= "#{association_id._singularize}_id" + options[:polymorphic_type_key] ||= "#{association_id._singularize}_type" + + if options.has_key? :ignore_duplicates + _logger_warn "DEPRECATION WARNING: please use :skip_duplicates instead of :ignore_duplicates" + options[:skip_duplicates] = options[:ignore_duplicates] + end + options[:skip_duplicates] = true unless options.has_key? :skip_duplicates + options[:dependent] = :destroy unless options.has_key? :dependent + options[:conditions] = sanitize_sql(options[:conditions]) + +# options[:finder_sql] ||= "(options[:polymorphic_key] + + options[:through] ||= build_join_table_symbol((options[:as]._pluralize or self.table_name), association_id) + options[:join_class_name] ||= options[:through]._classify + options[:table_aliases] ||= build_table_aliases([options[:through]] + options[:from]) + options[:select] ||= build_select(association_id, options[:table_aliases]) + + options[:through] = "#{options[:through]}_as_#{options[:singular_reverse_association_id]}" if options[:singular_reverse_association_id] + options[:through] = demodulate(options[:through]).to_sym + + options[:extend] = spiked_create_extension_module(association_id, Array(options[:extend]) + Array(extension)) + options[:join_extend] = spiked_create_extension_module(association_id, Array(options[:join_extend]), "Join") + options[:parent_extend] = spiked_create_extension_module(association_id, Array(options[:parent_extend]), "Parent") + + # create the reflection object + returning(create_reflection(:has_many_polymorphs, association_id, options, self)) do |reflection| + if defined? Dependencies and RAILS_ENV == "development" + _logger_warn "DEPRECATION WARNING: \"has_many_polymorphs_cache_classes =\" no longer has any effect. Please use \"config.cache_classes = true\" in the regular environment config (not in the \"after_initialize\" block)." if ActiveRecord::Associations::ClassMethods.has_many_polymorphs_cache_classes + inject_dependencies(association_id, reflection) if Dependencies.mechanism == :load + end + + # set up the other related associations + create_join_association(association_id, reflection) + create_has_many_through_associations_for_parent_to_children(association_id, reflection) + create_has_many_through_associations_for_children_to_parent(association_id, reflection) + end + end + + private + + ############## + # table mapping for use at instantiation point + + def build_table_aliases(from) + # for the targets + returning({}) do |aliases| + from.map(&:to_s).sort.map(&:to_sym).each_with_index do |plural, t_index| + table = plural._as_class.table_name + plural._as_class.columns.map(&:name).each_with_index do |field, f_index| + aliases["#{table}.#{field}"] = "t#{t_index}_r#{f_index}" + end + end + end + end + + def build_select(association_id, aliases) + # cause instantiate has to know which reflection the results are coming from + (["\'#{self.name}\' AS polymorphic_parent_class", + "\'#{association_id}\' AS polymorphic_association_id"] + + aliases.map do |table, _alias| + "#{table} AS #{_alias}" + end.sort).join(", ") + end + + ############## + # model caching + + def inject_dependencies(association_id, reflection) + _logger_debug "has_many_polymorphs: injecting dependencies" + requirements = [self, reflection.klass].map{|klass| [klass, klass.base_class]}.flatten.uniq + +# below, a contributed fix for a bug in doubles that I can't reproduce +# parents = all_classes_for(association_id, reflection) +# if (parents - requirements).empty? +# requirements = (requirements - [parents[0]]) +# parents = [parents[0]] +# else +# parents = (parents - requirements) +# end +# +# parents.each do |target_klass| +# Dependencies.inject_dependency(target_klass, *requirements) +# end + + (all_classes_for(association_id, reflection) - requirements).each do |target_klass| + Dependencies.inject_dependency(target_klass, *requirements) + end + end + + ################# + # macro sub-builders + + def create_join_association(association_id, reflection) + + options = {:foreign_key => reflection.options[:foreign_key], + :dependent => reflection.options[:dependent], + :class_name => reflection.klass.name, + :extend => reflection.options[:join_extend], +# :limit => reflection.options[:limit], +# :offset => reflection.options[:offset], + :order => devolve(association_id, reflection, reflection.options[:order], reflection.klass), + :conditions => devolve(association_id, reflection, reflection.options[:conditions], reflection.klass) + } + + if reflection.options[:foreign_type_key] + type_check = "#{reflection.options[:foreign_type_key]} = #{quote_value(self.base_class.name)}" + conjunction = options[:conditions] ? " AND " : nil + options[:conditions] = "#{options[:conditions]}#{conjunction}#{type_check}" + end + + has_many(reflection.options[:through], options) + inject_before_save_into_join_table(association_id, reflection) + end + + def inject_before_save_into_join_table(association_id, reflection) + sti_hook = "sti_class_rewrite" + rewrite_procedure = %[self.send(:#{association_id._singularize}_type=, self.#{association_id._singularize}_type.constantize.base_class.name)] + + # XXX should be abstracted? + reflection.klass.class_eval %[ + unless instance_methods.include? "before_save_with_#{sti_hook}" + if instance_methods.include? "before_save" + alias_method :before_save_without_#{sti_hook}, :before_save + def before_save_with_#{sti_hook} + before_save_without_#{sti_hook} + #{rewrite_procedure} + end + else + def before_save_with_#{sti_hook} + #{rewrite_procedure} + end + end + alias_method :before_save, :before_save_with_#{sti_hook} + end + ] + end + + def create_has_many_through_associations_for_children_to_parent(association_id, reflection) + + child_pluralization_map(association_id, reflection).each do |plural, singular| + if singular == reflection.options[:as] + raise PolymorphicError, if reflection.options[:is_double] + "You can't give either of the sides in a double-polymorphic join the same name as any of the individual target classes." + else + "You can't have a self-referential polymorphic has_many :through without renaming the non-polymorphic foreign key in the join model." + end + end + + parent = self + plural._as_class.instance_eval do + # this shouldn't be called at all during doubles; there is no way to traverse to a double polymorphic parent (XXX is that right?) + unless reflection.options[:is_double] or reflection.options[:conflicts].include? self.name.tableize.to_sym + + # the join table + through = "#{reflection.options[:through]}#{'_as_child' if parent == self}".to_sym + has_many(through, + :as => association_id._singularize, + :class_name => reflection.klass.name, + :dependent => reflection.options[:dependent], + :extend => reflection.options[:join_extend], +# :limit => reflection.options[:limit], +# :offset => reflection.options[:offset], + :order => devolve(association_id, reflection, reflection.options[:order], reflection.klass), + :conditions => devolve(association_id, reflection, reflection.options[:conditions], reflection.klass) + ) + + # the association to the collection parents + association = "#{reflection.options[:as]._pluralize}#{"_of_#{association_id}" if reflection.options[:rename_individual_collections]}".to_sym + has_many(association, + :through => through, + :class_name => parent.name, + :source => reflection.options[:as], + :foreign_key => reflection.options[:foreign_key] , + :extend => reflection.options[:parent_extend], + :order => reflection.options[:parent_order], + :offset => reflection.options[:parent_offset], + :limit => reflection.options[:parent_limit], + :group => reflection.options[:parent_group]) + + end + end + end + end + + def create_has_many_through_associations_for_parent_to_children(association_id, reflection) + child_pluralization_map(association_id, reflection).each do |plural, singular| + #puts ":source => #{child}" + current_association = demodulate(child_association_map(association_id, reflection)[plural]) + source = demodulate(singular) + + if reflection.options[:conflicts].include? plural + # XXX check this + current_association = "#{association_id._singularize}_#{current_association}" if reflection.options[:conflicts].include? self.name.tableize.to_sym + source = "#{source}_as_#{association_id._singularize}".to_sym + end + + # make push/delete accessible from the individual collections but still operate via the general collection + extension_module = self.class_eval %[ + module #{self.name + current_association._classify + "PolymorphicChildAssociationExtension"} + def push *args; proxy_owner.send(:#{association_id}).send(:push, *args).select{|x| x.is_a? #{singular._classify}}; end + alias :<< :push + def delete *args; proxy_owner.send(:#{association_id}).send(:delete, *args); end + def clear; proxy_owner.send(:#{association_id}).send(:clear, #{singular._classify}); end + self + end] + + has_many(current_association.to_sym, + :through => reflection.options[:through], + :source => association_id._singularize, + :source_type => plural._as_class.base_class.name, + :extend => (Array(extension_module) + reflection.options[:extend]), + :limit => reflection.options[:limit], +# :offset => reflection.options[:offset], + :order => devolve(association_id, reflection, reflection.options[:order], plural._as_class), + :conditions => devolve(association_id, reflection, reflection.options[:conditions], plural._as_class), + :group => devolve(association_id, reflection, reflection.options[:group], plural._as_class) + ) + + end + end + + ############## + # some support methods + + def child_pluralization_map(association_id, reflection) + Hash[*reflection.options[:from].map do |plural| + [plural, plural._singularize] + end.flatten] + end + + def child_association_map(association_id, reflection) + Hash[*reflection.options[:from].map do |plural| + [plural, "#{association_id._singularize.to_s + "_" if reflection.options[:rename_individual_collections]}#{plural}".to_sym] + end.flatten] + end + + def demodulate(s) + s.to_s.gsub('/', '_').to_sym + end + + def build_join_table_symbol(a, b) + [a.to_s, b.to_s].sort.join("_").to_sym + end + + def all_classes_for(association_id, reflection) + klasses = [self, reflection.klass, *child_pluralization_map(association_id, reflection).keys.map(&:_as_class)] + klasses += klasses.map(&:base_class) + klasses.uniq + end + + def devolve(association_id, reflection, string, klass) + return unless string + (all_classes_for(association_id, reflection) - # the join class must always be preserved + [klass, klass.base_class, reflection.klass, reflection.klass.base_class]).map do |klass| + klass.columns.map do |column| + [klass.table_name, column.name] + end.map do |table, column| + ["#{table}.#{column}", "`#{table}`.#{column}", "#{table}.`#{column}`", "`#{table}`.`#{column}`"] + end + end.flatten.sort_by(&:size).reverse.each do |quoted_reference| + string.gsub!(quoted_reference, "NULL") + end + string + end + + def verify_pluralization_of(sym) + sym = sym.to_s + singular = sym.singularize + plural = singular.pluralize + raise PolymorphicError, "Pluralization rules not set up correctly. You passed :#{sym}, which singularizes to :#{singular}, but that pluralizes to :#{plural}, which is different. Maybe you meant :#{plural} to begin with?" unless sym == plural + end + + def spiked_create_extension_module(association_id, extensions, identifier = nil) + module_extensions = extensions.select{|e| e.is_a? Module} + proc_extensions = extensions.select{|e| e.is_a? Proc } + + # support namespaced anonymous blocks as well as multiple procs + proc_extensions.each_with_index do |proc_extension, index| + module_name = "#{self.to_s}#{association_id._classify}Polymorphic#{identifier}AssociationExtension#{index}" + the_module = self.class_eval "module #{module_name}; self; end" # haha + the_module.class_eval &proc_extension + module_extensions << the_module + end + module_extensions + end + + end +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/configuration.rb b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/configuration.rb new file mode 100644 index 00000000..913c7064 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/configuration.rb @@ -0,0 +1,18 @@ + +### deprecated + +if defined? Rails::Configuration + class Rails::Configuration + def has_many_polymorphs_cache_classes= *args + ::ActiveRecord::Associations::ClassMethods.has_many_polymorphs_cache_classes = *args + end + end +end + +module ActiveRecord + module Associations + module ClassMethods + mattr_accessor :has_many_polymorphs_cache_classes + end + end +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/debugging_tools.rb b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/debugging_tools.rb new file mode 100644 index 00000000..b78a7bf4 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/debugging_tools.rb @@ -0,0 +1,72 @@ + + +class << ActiveRecord::Base + COLLECTION_METHODS = [:belongs_to, :has_many, :has_and_belongs_to_many, :has_one, + :has_many_polymorphs, :acts_as_double_polymorphic_join].each do |method_name| + alias_method "original_#{method_name}".to_sym, method_name + undef_method method_name + end + + unless defined? GENERATED_CODE_DIR + # automatic code generation for debugging + # you will get a folder "generated_models" in RAILS_ROOT containing valid Ruby files + # explaining all ActiveRecord relationships set up by the plugin, as well as listing the + # line in the plugin that made each particular macro call + GENERATED_CODE_DIR = "#{RAILS_ROOT}/generated_models" + + begin + system "rm -rf #{GENERATED_CODE_DIR}" + Dir.mkdir GENERATED_CODE_DIR + rescue Errno::EACCES + _logger_warn "no permissions for generated code dir: #{GENERATED_CODE_DIR}" + end + + if File.exist? GENERATED_CODE_DIR + alias :original_method_missing :method_missing + def method_missing(method_name, *args, &block) + if COLLECTION_METHODS.include? method_name.to_sym + Dir.chdir GENERATED_CODE_DIR do + filename = "#{demodulate(self.name.underscore)}.rb" + contents = File.open(filename).read rescue "\nclass #{self.name}\n\nend\n" + line = caller[1][/\:(\d+)\:/, 1] + contents[-5..-5] = "\n #{method_name} #{args[0..-2].inspect[1..-2]},\n #{args[-1].inspect[1..-2].gsub(" :", "\n :").gsub("=>", " => ")}\n#{ block ? " #{block.inspect.sub(/\@.*\//, '@')}\n" : ""} # called from line #{line}\n\n" + File.open(filename, "w") do |file| + file.puts contents + end + end + # doesn't handle blocks cause we can't introspect on code like that in Ruby without hackery and dependencies + self.send("original_#{method_name}", *args, &block) + else + self.send(:original_method_missing, method_name, *args, &block) + end + end + end + + end +end + +# and have a debugger enabled +case ENV['DEBUG'] + when "ruby-debug" + require 'rubygems' + require 'ruby-debug' + Debugger.start + puts "Notice; ruby-debug enabled." + when "trace" + puts "Notice; method tracing enabled" + $debug_trace_indent = 0 + set_trace_func (proc do |event, file, line, id, binding, classname| + if id.to_s =~ /instantiate/ #/IRB|Wirble|RubyLex|RubyToken|Logger|ConnectionAdapters|SQLite3|MonitorMixin|Benchmark|Inflector|Inflections/ + if event == 'call' + puts (" " * $debug_trace_indent) + "#{event}ed #{classname}\##{id} from #{file.split('/').last}::#{line}" + $debug_trace_indent += 1 + elsif event == 'return' + $debug_trace_indent -= 1 unless $debug_trace_indent == 0 + puts (" " * $debug_trace_indent) + "#{event}ed #{classname}\##{id}" + end + end + end) + when "dependencies" + puts "Notice; dependency activity being logged" + (::Dependencies.log_activity = true) rescue nil +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/dependencies.rb b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/dependencies.rb new file mode 100644 index 00000000..6efcb09e --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/dependencies.rb @@ -0,0 +1,30 @@ + +module Dependencies + + #### dependency injection + + mattr_accessor :injection_graph + self.injection_graph = Hash.new([]) + + def inject_dependency(target, *requirements) + target, requirements = target.to_s, requirements.map(&:to_s) + injection_graph[target] = ((injection_graph[target] + requirements).uniq - [target]) + requirements.each {|requirement| mark_for_unload requirement } +# _logger_debug "has_many_polymorphs: injection graph: #{injection_graph.inspect}" + end + + def new_constants_in_with_injection(*descs, &block) # chain +# _logger_debug "has_many_polymorphs: NEW: autoloaded constants: #{autoloaded_constants.inspect}; #{explicitly_unloadable_constants.inspect}" if (autoloaded_constants + explicitly_unloadable_constants).any? + returning(new_constants_in_without_injection(*descs, &block)) do |found| +# _logger_debug "has_many_polymorphs: new constants: #{found.inspect}" if found.any? + found.each do |constant| + injection_graph[constant].each do |requirement| + requirement.constantize + # _logger_debug "has_many_polymorphs: constantized #{requirement}" + end + end + end + end + alias_method_chain :new_constants_in, :injection + +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/rake_task_redefine_task.rb b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/rake_task_redefine_task.rb new file mode 100644 index 00000000..fce969e3 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/rake_task_redefine_task.rb @@ -0,0 +1,25 @@ + +# http://www.bigbold.com/snippets/posts/show/2032 +module Rake + module TaskManager + def redefine_task(task_class, args, &block) + task_name, deps = resolve_args(args) + task_name = task_class.scope_name(@scope, task_name) + deps = [deps] unless deps.respond_to?(:to_ary) + deps = deps.collect {|d| d.to_s } + task = @tasks[task_name.to_s] = task_class.new(task_name, self) + task.application = self + task.add_comment(@last_comment) + @last_comment = nil + task.enhance(deps, &block) + task + end + end + class Task + class << self + def redefine_task(args, &block) + Rake.application.redefine_task(self, args, &block) + end + end + end +end \ No newline at end of file diff --git a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/reflection.rb b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/reflection.rb new file mode 100644 index 00000000..02303be2 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/reflection.rb @@ -0,0 +1,41 @@ +module ActiveRecord + module Reflection + + module ClassMethods + def create_reflection(macro, name, options, active_record) + case macro + when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many + reflection = AssociationReflection.new(macro, name, options, active_record) + when :composed_of + reflection = AggregateReflection.new(macro, name, options, active_record) + ###### + when :has_many_polymorphs + reflection = PolymorphicReflection.new(macro, name, options, active_record) + ###### + end + write_inheritable_hash :reflections, name => reflection + reflection + end + end + + class PolymorphicError < ActiveRecordError + end + + class PolymorphicReflection < AssociationReflection + def check_validity! + # nothing + end + + # these are kind of shady but it lets us inherit more directly + def source_reflection + self + end + + def class_name + @class_name ||= options[:join_class_name] + end + + end + + end +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/support_methods.rb b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/support_methods.rb new file mode 100644 index 00000000..cd56011f --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/support_methods.rb @@ -0,0 +1,57 @@ + +# hope these don't mess anyone up + +class String + def _as_class + # classify expects self to be plural + self.classify.constantize + end +# def _as_base_class; _as_class.base_class; end + alias :_singularize :singularize + alias :_pluralize :pluralize + alias :_classify :classify +end + +class Symbol + def _as_class; self.to_s._as_class; end +# def _as_base_class; self.to_s._as_base_class; end + def _singularize; self.to_s.singularize.to_sym; end + def _pluralize; self.to_s.pluralize.to_sym; end + def _classify; self.to_s.classify; end +end + +class Array + def _flatten_once + self.inject([]){|r, el| r + Array(el)} + end +end + +class Hash + def _select + Hash[*self.select do |key, value| + yield key, value + end._flatten_once] + end +end + +class Object + def _metaclass; (class << self; self; end); end + + def _logger_debug s + ::ActiveRecord::Base.logger.debug(s) if ::ActiveRecord::Base.logger + end + def _logger_warn s + if ::ActiveRecord::Base.logger + ::ActiveRecord::Base.logger.warn(s) + else + $stderr.puts("has_many_polymorphs: #{s}") + end + end + +end + +class ActiveRecord::Base + def _base_class_name + self.class.base_class.name.to_s + end +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/aquatic/fish.yml b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/aquatic/fish.yml index 713d9127..3974a672 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/aquatic/fish.yml +++ b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/aquatic/fish.yml @@ -2,7 +2,11 @@ swimmy: id: 1 name: Swimmy speed: 10 + created_at: "2007-02-01 12:00:00" + updated_at: "2007-02-04 10:00:00" jaws: id: 2 name: Jaws - speed: 20 \ No newline at end of file + speed: 20 + created_at: "2007-02-02 12:00:00" + updated_at: "2007-02-03 10:00:00" diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/aquatic/whales.yml b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/aquatic/whales.yml index be296d47..bded734e 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/aquatic/whales.yml +++ b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/aquatic/whales.yml @@ -1,3 +1,5 @@ shamu: id: 1 name: Shamu + created_at: "2007-03-01 12:00:00" + updated_at: "2007-03-04 10:00:00" diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/bow_wows.yml b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/bow_wows.yml index 81759008..00be9d88 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/bow_wows.yml +++ b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/bow_wows.yml @@ -1,6 +1,10 @@ rover: id: 1 name: Rover + created_at: "2007-01-01 12:00:00" + updated_at: "2007-01-04 10:00:00" spot: id: 2 - name: Spot \ No newline at end of file + name: Spot + created_at: "2007-01-02 12:00:00" + updated_at: "2007-01-03 10:00:00" diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/cats.yml b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/cats.yml index adf3ead7..551d7460 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/cats.yml +++ b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/cats.yml @@ -2,7 +2,11 @@ chloe: id: 1 cat_type: Kitten name: Chloe + created_at: "2007-04-01 12:00:00" + updated_at: "2007-04-04 10:00:00" alice: id: 2 cat_type: Kitten - name: Alice \ No newline at end of file + name: Alice + created_at: "2007-04-02 12:00:00" + updated_at: "2007-04-03 10:00:00" diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/frogs.yml b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/frogs.yml index 145700f4..e9d37d7c 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/frogs.yml +++ b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/frogs.yml @@ -1,3 +1,5 @@ froggy: id: 1 name: Froggy + created_at: "2007-05-01 12:00:00" + updated_at: "2007-05-04 10:00:00" diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/petfoods.yml b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/petfoods.yml index bb174ea8..a117d294 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/petfoods.yml +++ b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/petfoods.yml @@ -1,6 +1,11 @@ kibbles: the_petfood_primary_key: 1 name: Kibbles + created_at: "2007-06-01 12:00:00" + updated_at: "2007-06-04 10:00:00" bits: the_petfood_primary_key: 2 - name: Bits \ No newline at end of file + name: Bits + created_at: "2007-06-02 12:00:00" + updated_at: "2007-06-03 10:00:00" + \ No newline at end of file diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/wild_boars.yml b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/wild_boars.yml index 39f12b18..73fd3e2e 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/wild_boars.yml +++ b/tracks/vendor/plugins/has_many_polymorphs/test/fixtures/wild_boars.yml @@ -1,6 +1,10 @@ puma: id: 1 name: Puma + created_at: "2007-07-01 12:00:00" + updated_at: "2007-07-04 10:00:00" jacrazy: id: 2 name: Jacrazy + created_at: "2007-07-02 12:00:00" + updated_at: "2007-07-03 10:00:00" diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/models/aquatic/whale.rb b/tracks/vendor/plugins/has_many_polymorphs/test/models/aquatic/whale.rb index 698ca6d4..a02c328c 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/models/aquatic/whale.rb +++ b/tracks/vendor/plugins/has_many_polymorphs/test/models/aquatic/whale.rb @@ -5,7 +5,9 @@ require 'aquatic/pupils_whale' class Aquatic::Whale < ActiveRecord::Base has_many_polymorphs(:aquatic_pupils, :from => [:dogs, :"aquatic/fish"], - :through => "aquatic/pupils_whales") do - def blow; "result"; end - end + :through => "aquatic/pupils_whales") do + def a_method + :correct_block_result + end + end end diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/models/beautiful_fight_relationship.rb b/tracks/vendor/plugins/has_many_polymorphs/test/models/beautiful_fight_relationship.rb index c1935b30..b678c982 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/models/beautiful_fight_relationship.rb +++ b/tracks/vendor/plugins/has_many_polymorphs/test/models/beautiful_fight_relationship.rb @@ -1,4 +1,6 @@ +require 'extension_module' + class BeautifulFightRelationship < ActiveRecord::Base set_table_name 'keep_your_enemies_close' @@ -8,6 +10,17 @@ class BeautifulFightRelationship < ActiveRecord::Base # are not supported by Rails acts_as_double_polymorphic_join :enemies => [:dogs, :kittens, :frogs], - :protectors => [:wild_boars, :kittens, :"aquatic/fish", :dogs] + :protectors => [:wild_boars, :kittens, :"aquatic/fish", :dogs], + :enemies_extend => [ExtensionModule, proc {}], + :protectors_extend => proc { + def a_method + :correct_proc_result + end + }, + :join_extend => proc { + def a_method + :correct_join_result + end + } end diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/models/canine.rb b/tracks/vendor/plugins/has_many_polymorphs/test/models/canine.rb new file mode 100644 index 00000000..b0010160 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/test/models/canine.rb @@ -0,0 +1,9 @@ +class Canine < ActiveRecord::Base + self.abstract_class = true + + def an_abstract_method + :correct_abstract_method_response + end + +end + diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/models/dog.rb b/tracks/vendor/plugins/has_many_polymorphs/test/models/dog.rb index 6f2da737..7f027237 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/models/dog.rb +++ b/tracks/vendor/plugins/has_many_polymorphs/test/models/dog.rb @@ -1,6 +1,8 @@ -class Dog < ActiveRecord::Base - attr_accessor :after_find_test, :after_initialize_test +require 'canine' + +class Dog < Canine + attr_accessor :after_find_test, :after_initialize_test set_table_name "bow_wows" def after_find diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/models/petfood.rb b/tracks/vendor/plugins/has_many_polymorphs/test/models/petfood.rb index fa8b0f91..1b53d1d7 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/models/petfood.rb +++ b/tracks/vendor/plugins/has_many_polymorphs/test/models/petfood.rb @@ -8,14 +8,31 @@ require 'dog' require 'wild_boar' require 'kitten' require 'tabby' +require 'extension_module' +require 'other_extension_module' class Petfood < ActiveRecord::Base set_primary_key 'the_petfood_primary_key' has_many_polymorphs :eaters, - :from => [:dogs, :petfoods, :wild_boars, :kittens, - :tabbies, :"aquatic/fish"], - :dependent => :destroy, - :rename_individual_collections => true, - :acts_as => :foodstuff, - :foreign_key => "foodstuff_id" -end + :from => [:dogs, :petfoods, :wild_boars, :kittens, + :tabbies, :"aquatic/fish"], +# :dependent => :destroy, :destroy is now the default + :rename_individual_collections => true, + :as => :foodstuff, + :foreign_key => "foodstuff_id", + :ignore_duplicates => false, + :conditions => "NULL IS NULL", + :order => "eaters_foodstuffs.updated_at ASC", + :parent_order => "the_petfood_primary_key DESC", + :extend => [ExtensionModule, OtherExtensionModule, proc {}], + :join_extend => proc { + def a_method + :correct_join_result + end + }, + :parent_extend => proc { + def a_method + :correct_parent_proc_result + end + } + end diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/modules/extension_module.rb b/tracks/vendor/plugins/has_many_polymorphs/test/modules/extension_module.rb new file mode 100644 index 00000000..7cb4eff4 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/test/modules/extension_module.rb @@ -0,0 +1,9 @@ + +module ExtensionModule + def a_method + :correct_module_result + end + def self.a_method + :incorrect_module_result + end +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/modules/other_extension_module.rb b/tracks/vendor/plugins/has_many_polymorphs/test/modules/other_extension_module.rb new file mode 100644 index 00000000..16313bd8 --- /dev/null +++ b/tracks/vendor/plugins/has_many_polymorphs/test/modules/other_extension_module.rb @@ -0,0 +1,9 @@ + +module OtherExtensionModule + def another_method + :correct_other_module_result + end + def self.another_method + :incorrect_other_module_result + end +end diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/schema.rb b/tracks/vendor/plugins/has_many_polymorphs/test/schema.rb index 12123532..c9ed6d65 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/schema.rb +++ b/tracks/vendor/plugins/has_many_polymorphs/test/schema.rb @@ -1,23 +1,33 @@ ActiveRecord::Schema.define(:version => 0) do create_table :petfoods, :force => true, :primary_key => :the_petfood_primary_key do |t| t.column :name, :string + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false end create_table :bow_wows, :force => true do |t| t.column :name, :string + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false end create_table :cats, :force => true do |t| t.column :name, :string t.column :cat_type, :string + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false end create_table :frogs, :force => true do |t| t.column :name, :string + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false end create_table :wild_boars, :force => true do |t| t.column :name, :string + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false end create_table :eaters_foodstuffs, :force => true do |t| @@ -25,21 +35,29 @@ ActiveRecord::Schema.define(:version => 0) do t.column :eater_id, :integer t.column :some_attribute, :integer, :default => 0 t.column :eater_type, :string + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false end create_table :fish, :force => true do |t| t.column :name, :string t.column :speed, :integer + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false end create_table :whales, :force => true do |t| t.column :name, :string + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false end create_table :little_whale_pupils, :force => true do |t| t.column :whale_id, :integer t.column :aquatic_pupil_id, :integer t.column :aquatic_pupil_type, :string + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false end create_table :keep_your_enemies_close, :force => true do |t| @@ -47,6 +65,8 @@ ActiveRecord::Schema.define(:version => 0) do t.column :enemy_type, :string t.column :protector_id, :integer t.column :protector_type, :string + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false end end diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/test_helper.rb b/tracks/vendor/plugins/has_many_polymorphs/test/test_helper.rb index 712b6926..92f115c3 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/test_helper.rb +++ b/tracks/vendor/plugins/has_many_polymorphs/test/test_helper.rb @@ -1,20 +1,31 @@ -require 'pathname' -# default test helper + +begin + require 'rubygems' + require 'ruby-debug' + Debugger.start +rescue Object +end + +# load the applicaiton's test helper begin require File.dirname(__FILE__) + '/../../../../test/test_helper' rescue LoadError require '~/projects/miscellaneous/cookbook/test/test_helper' end +WORKING_DIR = File.dirname(__FILE__) + Inflector.inflections {|i| i.irregular 'fish', 'fish' } # fixtures -$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/") +$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path = WORKING_DIR + "/fixtures") # models -$LOAD_PATH.unshift("#{Pathname.new(__FILE__).dirname.to_s}/models") +$LOAD_PATH.unshift(WORKING_DIR + "/models") +# extension modules +$LOAD_PATH.unshift(WORKING_DIR + "/modules") class Test::Unit::TestCase - self.use_transactional_fixtures = true # must stay true for tests to run on postgres or sqlite3 + self.use_transactional_fixtures = (not ActiveRecord::Base.connection.is_a? ActiveRecord::ConnectionAdapters::MysqlAdapter) self.use_instantiated_fixtures = false end diff --git a/tracks/vendor/plugins/has_many_polymorphs/test/unit/polymorph_test.rb b/tracks/vendor/plugins/has_many_polymorphs/test/unit/polymorph_test.rb index 750a9295..c84b03f5 100644 --- a/tracks/vendor/plugins/has_many_polymorphs/test/unit/polymorph_test.rb +++ b/tracks/vendor/plugins/has_many_polymorphs/test/unit/polymorph_test.rb @@ -6,13 +6,9 @@ class PolymorphTest < Test::Unit::TestCase :"aquatic/fish", :"aquatic/whales", :"aquatic/little_whale_pupils", :keep_your_enemies_close require 'beautiful_fight_relationship' - - # to-do: finder queries on the collection - # order-mask column on the join table for polymorphic order - # rework load order so you could push and pop without ever loading the whole collection - # so that limit works in a sane way - + def setup + @association_error = ActiveRecord::Associations::PolymorphicError @kibbles = Petfood.find(1) @bits = Petfood.find(2) @shamu = Aquatic::Whale.find(1) @@ -25,8 +21,8 @@ class PolymorphTest < Test::Unit::TestCase @froggy = Frog.find(1) @join_count = EatersFoodstuff.count - @l = @kibbles.eaters.length - @m = @bits.eaters.count + @l = @kibbles.eaters.size + @m = @bits.eaters.size end def test_all_relationship_validities @@ -53,21 +49,16 @@ class PolymorphTest < Test::Unit::TestCase assert_equal @l += 1, @kibbles.eaters.count @kibbles.reload - assert_equal @l, @kibbles.eaters.count - + assert_equal @l, @kibbles.eaters.count end def test_duplicate_assignment - # try to add a duplicate item + # try to add a duplicate item when :ignore_duplicates is false @kibbles.eaters.push(@alice) assert @kibbles.eaters.include?(@alice) @kibbles.eaters.push(@alice) - assert_equal @l + 1, @kibbles.eaters.count - assert_equal @join_count + 1, EatersFoodstuff.count - - @kibbles.reload - assert_equal @l + 1, @kibbles.eaters.count - assert_equal @join_count + 1, EatersFoodstuff.count + assert_equal @l + 2, @kibbles.eaters.count + assert_equal @join_count + 2, EatersFoodstuff.count end def test_create_and_push @@ -97,31 +88,27 @@ class PolymorphTest < Test::Unit::TestCase assert @join_record.id assert_equal @join_count + 1, EatersFoodstuff.count - # has the parent changed if we don't reload? - assert_equal @m, @bits.eaters.count - - # if we do reload, is the new association there? - # XXX no, because TestCase breaks reload. it works fine in the app. + # not reloaded + assert_equal @m, @bits.eaters.size + assert_equal @m + 1, @bits.eaters.count # SQL :) - assert_equal Petfood, @bits.eaters.reload.class - assert_equal @m + 1, @bits.eaters.count + # is the new association there? + assert @bits.eaters.reload assert @bits.eaters.include?(@chloe) - -# puts "XXX #{EatersFoodstuff.count}" - end - - def test_add_unsaved - # add an unsaved item - assert @bits.eaters << Kitten.new(:name => "Bridget") - assert_nil Kitten.find_by_name("Bridget") - assert_equal @m + 1, @bits.eaters.count - assert @bits.save - @bits.reload - assert_equal @m + 1, @bits.eaters.count - - end +# not supporting this, since has_many :through doesn't support it either +# def test_add_unsaved +# # add an unsaved item +# assert @bits.eaters << Kitten.new(:name => "Bridget") +# assert_nil Kitten.find_by_name("Bridget") +# assert_equal @m + 1, @bits.eaters.count +# +# assert @bits.save +# @bits.reload +# assert_equal @m + 1, @bits.eaters.count +# +# end def test_self_reference assert @kibbles.eaters << @bits @@ -154,11 +141,10 @@ class PolymorphTest < Test::Unit::TestCase def test_clear @kibbles.eaters << [@chloe, @spot, @rover] @kibbles.reload - assert_equal 3, @kibbles.eaters.clear.size + assert @kibbles.eaters.clear.blank? assert @kibbles.eaters.blank? @kibbles.reload assert @kibbles.eaters.blank? - assert_equal 0, @kibbles.eaters.clear.size end def test_individual_collections @@ -169,7 +155,7 @@ class PolymorphTest < Test::Unit::TestCase assert 1, @rover.eaters_foodstuffs.count end - def test_invididual_collections_push + def test_individual_collections_push assert_equal [@chloe], (@kibbles.eater_kittens << @chloe) @kibbles.reload assert @kibbles.eaters.include?(@chloe) @@ -177,24 +163,31 @@ class PolymorphTest < Test::Unit::TestCase assert !@kibbles.eater_dogs.include?(@chloe) end - def test_invididual_collections_delete + def test_individual_collections_delete @kibbles.eaters << [@chloe, @spot, @rover] @kibbles.reload assert_equal [@chloe], @kibbles.eater_kittens.delete(@chloe) assert @kibbles.eater_kittens.empty? - assert !@kibbles.eater_kittens.delete(@chloe) + @kibbles.eater_kittens.delete(@chloe) # what should this return? @kibbles.reload assert @kibbles.eater_kittens.empty? assert @kibbles.eater_dogs.include?(@spot) end - def test_invididual_collections_clear + def test_individual_collections_clear @kibbles.eaters << [@chloe, @spot, @rover] @kibbles.reload - assert_equal [@chloe], @kibbles.eater_kittens.clear + + assert_equal [], @kibbles.eater_kittens.clear assert @kibbles.eater_kittens.empty? assert_equal 2, @kibbles.eaters.size + + assert @kibbles.eater_kittens.empty? + assert_equal 2, @kibbles.eaters.size + assert !@kibbles.eater_kittens.include?(@chloe) + assert !@kibbles.eaters.include?(@chloe) + @kibbles.reload assert @kibbles.eater_kittens.empty? assert_equal 2, @kibbles.eaters.size @@ -229,26 +222,20 @@ class PolymorphTest < Test::Unit::TestCase def test_normal_callbacks assert @rover.respond_to?(:after_initialize) - assert @rover.respond_to?(:after_find) - + assert @rover.respond_to?(:after_find) assert @rover.after_initialize_test assert @rover.after_find_test end - def test_our_callbacks + def test_model_callbacks_not_overridden_by_plugin_callbacks assert 0, @bits.eaters.count assert @bits.eaters.push(@rover) @bits.save - -# puts "Testing callbacks." @bits2 = Petfood.find_by_name("Bits") @bits.reload - assert rover = @bits2.eaters.select { |x| x.name == "Rover" }[0] assert rover.after_initialize_test assert rover.after_find_test -# puts "Done." - end def test_number_of_join_records @@ -268,8 +255,8 @@ class PolymorphTest < Test::Unit::TestCase @join_record.save! @bits.eaters.reload - assert_equal 'Puma', @puma.name - assert_equal 'Puma', @bits.eaters.first.name + assert_equal "Puma", @puma.name + assert_equal "Puma", @bits.eaters.first.name end def test_before_save_on_join_table_is_not_clobbered_by_sti_base_class_fix @@ -347,6 +334,10 @@ class PolymorphTest < Test::Unit::TestCase assert_equal 3, @alice.beautiful_fight_relationships.size end + def test_double_dependency_injection +# breakpoint + end + def test_double_collection_deletion @alice.enemies << @spot @alice.reload @@ -432,17 +423,12 @@ class PolymorphTest < Test::Unit::TestCase assert !@spot.protectors.include?(@alice) end - def test_hmp_passed_block_manipulates_proxy_class - assert_equal "result", @shamu.aquatic_pupils.blow - assert_raises(NoMethodError) { @kibbles.eaters.blow } - end - def test_collection_query_on_unsaved_record assert Dog.new.enemies.empty? assert Dog.new.foodstuffs_of_eaters.empty? end - def test_double_invididual_collections_push + def test_double_individual_collections_push assert_equal [@chloe], (@spot.protector_kittens << @chloe) @spot.reload assert @spot.protectors.include?(@chloe) @@ -456,22 +442,22 @@ class PolymorphTest < Test::Unit::TestCase assert !@spot.enemy_dogs.include?(@froggy) end - def test_double_invididual_collections_delete + def test_double_individual_collections_delete @spot.protectors << [@chloe, @puma] @spot.reload assert_equal [@chloe], @spot.protector_kittens.delete(@chloe) assert @spot.protector_kittens.empty? - assert !@spot.protector_kittens.delete(@chloe) + @spot.protector_kittens.delete(@chloe) # again, unclear what .delete should return @spot.reload assert @spot.protector_kittens.empty? assert @spot.wild_boars.include?(@puma) end - def test_double_invididual_collections_clear + def test_double_individual_collections_clear @spot.protectors << [@chloe, @puma, @alice] @spot.reload - assert_equal [@chloe, @alice], @spot.protector_kittens.clear.sort_by(&:id) + assert_equal [], @spot.protector_kittens.clear assert @spot.protector_kittens.empty? assert_equal 1, @spot.protectors.size @spot.reload @@ -481,7 +467,156 @@ class PolymorphTest < Test::Unit::TestCase assert !@spot.protectors.include?(@chloe) assert !@spot.protector_kittens.include?(@alice) assert !@spot.protectors.include?(@alice) + assert @spot.protectors.include?(@puma) + assert @spot.wild_boars.include?(@puma) end + def test_single_extensions + assert_equal :correct_block_result, @shamu.aquatic_pupils.a_method + @kibbles.eaters.push(@alice) + @kibbles.eaters.push(@spot) + assert_equal :correct_join_result, @kibbles.eaters_foodstuffs.a_method + assert_equal :correct_module_result, @kibbles.eaters.a_method + assert_equal :correct_other_module_result, @kibbles.eaters.another_method + @kibbles.eaters.each do |eater| + assert_equal :correct_join_result, eater.eaters_foodstuffs.a_method + end + assert_equal :correct_parent_proc_result, @kibbles.foodstuffs_of_eaters.a_method + assert_equal :correct_parent_proc_result, @kibbles.eaters.first.foodstuffs_of_eaters.a_method + end + + def test_double_extensions + assert_equal :correct_proc_result, @spot.protectors.a_method + assert_equal :correct_module_result, @spot.enemies.a_method + assert_equal :correct_join_result, @spot.beautiful_fight_relationships_as_enemy.a_method + assert_equal :correct_join_result, @spot.beautiful_fight_relationships_as_protector.a_method + assert_equal :correct_join_result, @froggy.beautiful_fight_relationships.a_method + assert_equal :correct_join_result, @froggy.beautiful_fight_relationships_as_enemy.a_method + assert_raises(NoMethodError) {@froggy.beautiful_fight_relationships_as_protector.a_method} + end + + def test_pluralization_checks + assert_raises(@association_error) { + eval "class SomeModel < ActiveRecord::Base + has_many_polymorphs :polymorphs, :from => [:dog, :cats] + end" } + assert_raises(@association_error) { + eval "class SomeModel < ActiveRecord::Base + has_many_polymorphs :polymorph, :from => [:dogs, :cats] + end" } + assert_raises(@association_error) { + eval "class SomeModel < ActiveRecord::Base + acts_as_double_polymorphic_join :polymorph => [:dogs, :cats], :unimorphs => [:dogs, :cats] + end" } + end + + def test_single_custom_finders + [@kibbles, @alice, @puma, @spot, @bits].each {|record| @kibbles.eaters << record; sleep 1} # XXX yeah i know + assert_equal @kibbles.eaters, @kibbles.eaters.find(:all, :order => "eaters_foodstuffs.created_at ASC") + assert_equal @kibbles.eaters.reverse, @kibbles.eaters.find(:all, :order => "eaters_foodstuffs.created_at DESC") + if ActiveRecord::Base.connection.is_a? ActiveRecord::ConnectionAdapters::MysqlAdapter + assert_equal @kibbles.eaters.sort_by(&:created_at), @kibbles.eaters.find(:all, :order => "IFNULL(bow_wows.created_at,(IFNULL(petfoods.created_at,(IFNULL(wild_boars.created_at,(IFNULL(cats.created_at,fish.created_at))))))) ASC") + end + assert_equal @kibbles.eaters.select{|x| x.is_a? Petfood}, @kibbles.eater_petfoods.find(:all, :order => "eaters_foodstuffs.created_at ASC") + end + + def test_double_custom_finders + @spot.protectors << [@chloe, @puma, @alice] + assert_equal [@chloe], @spot.protectors.find(:all, :conditions => ["cats.name = ?", @chloe.name], :limit => 1) + assert_equal [], @spot.protectors.find(:all, :conditions => ["cats.name = ?", @chloe.name], :limit => 1, :offset => 1) + assert_equal 2, @spot.protectors.find(:all, :limit => 100, :offset => 1).size + end + + def test_single_custom_finder_parameters_carry_to_individual_relationships + # XXX test nullout here + end + + def test_double_custom_finder_parameters_carry_to_individual_relationships + # XXX test nullout here + end + + def test_include_doesnt_fail + assert_nothing_raised do + @spot.protectors.find(:all, :include => :wild_boars) + end + end + + def test_abstract_method + assert_equal :correct_abstract_method_response, @spot.an_abstract_method + end + + def test_missing_target_should_raise + @kibbles.eaters << [@kibbles, @alice, @puma, @spot, @bits] + @spot.destroy_without_callbacks + assert_raises(@association_error) { @kibbles.eaters.reload } +# assert_raises(@association_error) { @kibbles.eater_dogs.reload } # bah AR + end + + def test_lazy_loading_is_lazy + # XXX + end + + def test_push_with_skip_duplicates_false_doesnt_load_target + # XXX + end + + def test_association_foreign_key_is_sane + assert_equal "eater_id", Petfood.reflect_on_association(:eaters).association_foreign_key + end + + def test_reflection_instance_methods_are_sane + assert_equal EatersFoodstuff, Petfood.reflect_on_association(:eaters).klass + assert_equal EatersFoodstuff.name, Petfood.reflect_on_association(:eaters).class_name + end + + def test_parent_order_orders_parents + @alice.foodstuffs_of_eaters << Petfood.find(:all, :order => "the_petfood_primary_key ASC") + @alice.reload #not necessary + assert_equal [2,1], @alice.foodstuffs_of_eaters.map(&:id) + end + +# def test_polymorphic_include +# @kibbles.eaters << [@kibbles, @alice, @puma, @spot, @bits] +# assert @kibbles.eaters.include?(@kibbles.eaters_foodstuffs.find(:all, :include => :eater).first.eater) +# end +# +# def test_double_polymorphic_include +# end +# +# def test_single_child_include +# end +# +# def test_double_child_include +# end +# +# def test_single_include_from_parent +# end +# +# def test_double_include_from_parent +# end +# +# def test_meta_referential_single_include +# end +# +# def test_meta_referential_double_include +# end +# +# def test_meta_referential_single_include +# end +# +# def test_meta_referential_single_double_multi_include +# end +# +# def test_dont_ignore_duplicates +# end +# +# def test_ignore_duplicates +# end +# +# def test_tagging_system_generator +# end +# +# def test_tagging_system_library +# end end diff --git a/tracks/vendor/plugins/rails_rcov/MIT-LICENSE b/tracks/vendor/plugins/rails_rcov/MIT-LICENSE new file mode 100644 index 00000000..c61f2c86 --- /dev/null +++ b/tracks/vendor/plugins/rails_rcov/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2006 Coda Hale + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tracks/vendor/plugins/rails_rcov/README b/tracks/vendor/plugins/rails_rcov/README new file mode 100644 index 00000000..eb9d84c9 --- /dev/null +++ b/tracks/vendor/plugins/rails_rcov/README @@ -0,0 +1,86 @@ + = rails_rcov plugin for Rails + +rails_rcov provides easy-to-use Rake tasks to determine the code coverage of +your unit, functional, and integration tests using Mauricio Fernandez's rcov +tool. + +== Installation + +First, install rcov from Mauricio's web site +[http://eigenclass.org/hiki.rb?rcov]. Make sure it's on your system path, so +that typing +rcov+ on the command line actually runs it. THIS PLUGIN DOESN'T DO +ANYTHING BESIDES GENERATE ERRORS UNLESS YOU INSTALL RCOV FIRST. RCOV CONTAINS +ALL THE MAGIC, THIS PLUGIN JUST RUNS IT. + +Second, install this plugin. If your project is source-controlled by Subversion +(which it should be, really), the easiest way to install this is via Rails' +plugin script: + + ./script/plugin install -x http://svn.codahale.com/rails_rcov + +If you're not using Subversion, or if you don't want it adding +svn:externals in your project, remove the -x switch: + + ./script/plugin install http://svn.codahale.com/rails_rcov + +== Usage + +For each test:blah task you have for your Rails project, rails_rcov +adds two more: test:blah:rcov and test:blah:clobber_rcov. + +Running rake test:units:rcov, for example, will run your unit tests +through rcov and write the code coverage reports to +your_rails_app/coverage/units. + +Running test:units:clobber_rcov will erase the generated report for the +unit tests. + +Each rcov task takes two optional parameters: RCOV_PARAMS, whose argument is +passed along to rcov, and SHOW_ONLY, which limits the files displayed in the +report. + +RCOV_PARAMS: + # sort by coverage + rake test:units:rcov RCOV_PARAMS="--sort=coverage" + + # show callsites and hide fully covered files + rake test:units:rcov RCOV_PARAMS="--callsites --only-uncovered" + +Check the rcov documentation for more details. + +SHOW_ONLY is a comma-separated list of the files you'd like to see. Right now +there are four types of files rake_rcov recognizes: models, helpers, +controllers, and lib. These can be abbreviated to their first letters: + + # only show files from app/models + rake test:units:rcov SHOW_ONLY=models + + # only show files from app/helpers and app/controllers + rake test:units:rcov SHOW_ONLY=helpers,controllers + + # only show files from app/helpers and app/controllers, with less typing + rake test:units:rcov SHOW_ONLY=h,c + +Please note that rails_rcov has only been tested with a Bash shell, and any +other environment could well explode in your face. If you're having trouble +getting this to work on Windows, please take the time to figure out what's not +working. Most of the time it boils down to the different ways the Window shell +and the Bash shell escape metacharacters. Play around with the way rcov_rake +escapes data (like on line 73, or 78) and send me a fix. I don't have a working +Windows environment anymore, so leaving it up to me won't solve anything. ;-) + +== Resources + +=== Subversion + +* http://svn.codahale.com/rails_rcov + +=== Blog + +* http://blog.codahale.com + +== Credits + +Written by Coda Hale . Thanks to Nils Franzen for a Win32 +escaping patch. Thanks to Alex Wayne for suggesting how to make SHOW_ONLY not be +useless. \ No newline at end of file diff --git a/tracks/vendor/plugins/rails_rcov/tasks/rails_rcov.rake b/tracks/vendor/plugins/rails_rcov/tasks/rails_rcov.rake new file mode 100644 index 00000000..4fd798d3 --- /dev/null +++ b/tracks/vendor/plugins/rails_rcov/tasks/rails_rcov.rake @@ -0,0 +1,150 @@ +# This File Uses Magic +# ==================== +# Here's an example of how this file works. As an example, let's say you typed +# this into your terminal: +# +# $ rake --tasks +# +# The rake executable goes through all the various places .rake files can be, +# accumulates them all, and then runs them. When this file is loaded by Rake, +# it iterates through all the tasks, and for each task named 'test:blah' adds +# test:blah:rcov and test:blah:rcov_clobber. +# +# So you've seen all the tasks, and you type this into your terminal: +# +# $ rake test:units:rcov +# +# Rake does the same thing as above, but it runs the test:units:rcov task, which +# pretty much just does this: +# +# $ ruby [this file] [the test you want to run] [some options] +# +# Now this file is run via the Ruby interpreter, and after glomming up the +# options, it acts just like the Rake executable, with a slight difference: it +# passes all the arguments to rcov, not ruby, so all your unit tests get some +# rcov sweet loving. + +if ARGV.grep(/--run-rake-task=/).empty? + # Define all our Rake tasks + + require 'rake/clean' + require 'rcov/rcovtask' + + def to_rcov_task_sym(s) + s = s.gsub(/(test:)/,'') + s.empty? ? nil : s.intern + end + + def to_rcov_task_name(s) + s = s.gsub(/(test:)/,'') + s =~ /s$/i ? s[0..-2] : s + end + + def new_rcov_task(test_name) + output_dir = "./coverage/#{test_name.gsub('test:','')}" + CLOBBER.include(output_dir) + + # Add a task to run the rcov process + desc "Run all #{to_rcov_task_name(test_name)} tests with Rcov to measure coverage" + task :rcov => [:clobber_rcov] do |t| + run_code = '"' << File.expand_path(__FILE__) << '"' + run_code << " --run-rake-task=#{test_name}" + + params = String.new + if ENV['RCOV_PARAMS'] + params << ENV['RCOV_PARAMS'] + end + + # rake test:units:rcov SHOW_ONLY=models,controllers,lib,helpers + # rake test:units:rcov SHOW_ONLY=m,c,l,h + if ENV['SHOW_ONLY'] + show_only = ENV['SHOW_ONLY'].to_s.split(',').map{|x|x.strip} + if show_only.any? + reg_exp = [] + for show_type in show_only + reg_exp << case show_type + when 'm', 'models' : 'app\/models' + when 'c', 'controllers' : 'app\/controllers' + when 'h', 'helpers' : 'app\/helpers' + when 'l', 'lib' : 'lib' + else + show_type + end + end + reg_exp.map!{ |m| "(#{m})" } + params << " -x \\\"^(?!#{reg_exp.join('|')})\\\"" + end + end + + unless params.empty? + run_code << " --rcov-params=\"#{params}\"" + end + + ruby run_code + end + + # Add a task to clean up after ourselves + desc "Remove Rcov reports for #{to_rcov_task_name(test_name)} tests" + task :clobber_rcov do |t| + rm_r output_dir, :force => true + end + + # Link our clobber task to the main one + task :clobber => [:clobber_rcov] + end + + test_tasks = Rake::Task.tasks.select{ |t| t.comment && t.name =~ /^test/ } + for test_task in test_tasks + namespace :test do + if sym = to_rcov_task_sym(test_task.name) + namespace sym do + new_rcov_task(test_task.name) + end + end + end + end +else + # Load rake tasks, hijack ruby, and redirect the task through rcov + require 'rubygems' + require 'rake' + + module RcovTestSettings + class << self + attr_accessor :output_dir, :options + def to_params + "-o \"#{@output_dir}\" -T -x \"rubygems/*,rcov*\" --rails #{@options}" + end + end + + # load options and arguments from command line + unless (cmd_line = ARGV.grep(/--rcov-params=/)).empty? + @options = cmd_line.first.gsub(/--rcov-params=/, '') + end + end + + def is_windows? + processor, platform, *rest = RUBY_PLATFORM.split("-") + platform == 'mswin32' + end + + # intercept what Rake *would* be doing with Ruby, and send it through Rcov instead + module RakeFileUtils + alias :ruby_without_rcov :ruby + def ruby(*args, &block) + cmd = (is_windows? ? 'rcov.cmd' : 'rcov') << " #{RcovTestSettings.to_params} #{args}" + status = sh(cmd, {}, &block) + puts "View the full results at " + return status + end + end + + # read the test name and execute it (through Rcov) + unless (cmd_line = ARGV.grep(/--run-rake-task=/)).empty? + test_name = cmd_line.first.gsub(/--run-rake-task=/,'') + ARGV.clear; ARGV << test_name + RcovTestSettings.output_dir = File.expand_path("./coverage/#{test_name.gsub('test:','')}") + Rake.application.run + else + raise "No test to execute!" + end +end \ No newline at end of file