From f818abfd71717b94934c1bc1b1f2a03c0e87c8b9 Mon Sep 17 00:00:00 2001 From: binarylogic Date: Thu, 18 Sep 2008 21:03:50 -0400 Subject: [PATCH] Updated searchgasm --- README.rdoc | 4 +- app/models/user_group.rb | 1 + app/views/jquery/users/index.html.erb | 4 + app/views/non_ajax/users/index.html.erb | 4 + app/views/rails_ajax/users/index.html.erb | 9 +- config/boot.rb | 8 +- config/environment.rb | 2 +- public/javascripts/controls.js | 963 ++++++++++++++ public/javascripts/dragdrop.js | 972 ++++++++++++++ public/javascripts/effects.js | 1120 +++++++++++++++++ .../searchgasm-1.0.2/benchmarks/benchmark.rb | 43 - .../benchmarks/benchmark_helper.rb | 52 - .../searchgasm-1.0.2/benchmarks/profile.rb | 15 - .../searchgasm/active_record/associations.rb | 73 -- .../lib/searchgasm/utilities.rb | 30 - .../CHANGELOG.rdoc | 25 +- .../MIT-LICENSE | 0 .../Manifest | 8 +- .../README.rdoc | 44 +- .../Rakefile | 3 +- .../examples/README.rdoc | 0 .../init.rb | 0 .../lib/searchgasm.rb | 8 +- .../lib/searchgasm/active_record.rb | 0 .../searchgasm/active_record/associations.rb | 61 + .../lib/searchgasm/active_record/base.rb | 51 +- .../lib/searchgasm/condition/base.rb | 2 +- .../lib/searchgasm/condition/begins_with.rb | 0 .../lib/searchgasm/condition/child_of.rb | 0 .../lib/searchgasm/condition/contains.rb | 0 .../lib/searchgasm/condition/descendant_of.rb | 0 .../searchgasm/condition/does_not_equal.rb | 0 .../lib/searchgasm/condition/ends_with.rb | 0 .../lib/searchgasm/condition/equals.rb | 0 .../lib/searchgasm/condition/greater_than.rb | 0 .../condition/greater_than_or_equal_to.rb | 0 .../condition/inclusive_descendant_of.rb | 2 - .../lib/searchgasm/condition/keywords.rb | 0 .../lib/searchgasm/condition/less_than.rb | 0 .../condition/less_than_or_equal_to.rb | 0 .../lib/searchgasm/condition/sibling_of.rb | 2 - .../lib/searchgasm/condition/tree.rb | 0 .../lib/searchgasm/conditions/base.rb | 89 +- .../lib/searchgasm/conditions/protection.rb | 10 +- .../lib/searchgasm/config.rb | 72 ++ .../lib/searchgasm/core_ext/hash.rb | 0 .../lib/searchgasm/helpers.rb | 0 .../lib/searchgasm/helpers/control_types.rb | 0 .../searchgasm/helpers/control_types/link.rb | 0 .../searchgasm/helpers/control_types/links.rb | 66 +- .../helpers/control_types/remote_link.rb | 1 + .../helpers/control_types/remote_links.rb | 3 + .../helpers/control_types/remote_select.rb | 3 + .../helpers/control_types/select.rb | 3 + .../lib/searchgasm/helpers/form.rb | 15 +- .../lib/searchgasm/helpers/utilities.rb | 0 .../searchgasm-1.1.0/lib/searchgasm/search.rb | 7 + .../lib/searchgasm/search/base.rb | 73 +- .../lib/searchgasm/search/conditions.rb | 9 +- .../lib/searchgasm/search/ordering.rb | 0 .../lib/searchgasm/search/pagination.rb | 3 +- .../lib/searchgasm/search/protection.rb | 0 .../lib/searchgasm/shared/searching.rb | 43 + .../lib/searchgasm/shared/utilities.rb | 32 + .../lib/searchgasm/shared/virtual_classes.rb | 39 + .../lib/searchgasm/version.rb | 4 +- .../searchgasm.gemspec | 23 +- .../test/fixtures/accounts.yml | 0 .../test/fixtures/orders.yml | 0 .../test/fixtures/users.yml | 0 .../test/libs/acts_as_tree.rb | 0 .../test/libs/rexml_fix.rb | 0 .../test/test_active_record_associations.rb | 25 +- .../test/test_active_record_base.rb | 11 + .../test/test_condition_base.rb | 0 .../test/test_condition_types.rb | 0 .../test/test_conditions_base.rb | 42 +- .../test/test_conditions_protection.rb | 0 .../test/test_config.rb | 0 .../test/test_helper.rb | 0 .../test/test_search_base.rb | 27 +- .../test/test_search_conditions.rb | 0 .../test/test_search_ordering.rb | 0 .../test/test_search_pagination.rb | 11 + .../test/test_search_protection.rb | 0 .../test/text_config.rb | 0 .../actionmailer/lib/action_mailer/base.rb | 2 +- ...mplicitly_multipart_example.text.html.erb~ | 10 + vendor/rails/actionpack/CHANGELOG | 20 + vendor/rails/actionpack/Rakefile | 14 +- .../rails/actionpack/lib/action_controller.rb | 2 + .../assertions/routing_assertions.rb | 28 +- .../actionpack/lib/action_controller/base.rb | 8 +- .../lib/action_controller/benchmarking.rb | 61 +- .../lib/action_controller/cgi_process.rb | 3 +- .../lib/action_controller/rack_process.rb | 3 +- .../routing/recognition_optimisation.rb | 22 + .../action_controller/routing/route_set.rb | 4 +- .../action_controller/session/cookie_store.rb | 3 +- .../action_controller/session_management.rb | 4 + .../lib/action_controller/translation.rb | 13 + .../rails/actionpack/lib/action_view/base.rb | 10 +- .../action_view/helpers/asset_tag_helper.rb | 32 +- .../action_view/helpers/benchmark_helper.rb | 6 +- .../action_view/helpers/form_tag_helper.rb | 27 +- .../helpers/scriptaculous_helper.rb | 2 +- .../actionpack/lib/action_view/renderable.rb | 19 +- .../lib/action_view/renderable_partial.rb | 18 +- .../test/controller/routing_test.rb | 30 + .../controller/session/cookie_store_test.rb | 53 +- .../test/controller/translation_test.rb | 26 + .../test/template/form_tag_helper_test.rb | 11 +- vendor/rails/activerecord/CHANGELOG | 30 +- .../lib/active_record/aggregations.rb | 144 ++- .../lib/active_record/association_preload.rb | 13 +- .../lib/active_record/associations.rb | 73 +- .../associations/association_collection.rb | 14 +- .../associations/belongs_to_association.rb | 4 +- .../associations/has_many_association.rb | 5 +- .../has_many_through_association.rb | 9 +- .../associations/has_one_association.rb | 16 +- .../lib/active_record/attribute_methods.rb | 2 +- .../activerecord/lib/active_record/base.rb | 29 +- .../lib/active_record/calculations.rb | 2 +- .../abstract/database_statements.rb | 4 + .../abstract/schema_definitions.rb | 42 +- .../connection_adapters/abstract_adapter.rb | 6 +- .../connection_adapters/mysql_adapter.rb | 11 +- .../activerecord/lib/active_record/dirty.rb | 20 +- .../lib/active_record/dynamic_finder_match.rb | 3 +- .../lib/active_record/locale/en-US.yml | 29 +- .../lib/active_record/reflection.rb | 35 + .../lib/active_record/schema_dumper.rb | 2 +- .../lib/active_record/validations.rb | 97 +- .../test/cases/aggregations_test.rb | 39 + .../belongs_to_associations_test.rb | 13 + ...s_and_belongs_to_many_associations_test.rb | 12 + .../has_many_associations_test.rb | 7 + .../test/cases/associations_test.rb | 120 +- .../test/cases/attribute_methods_test.rb | 9 + .../activerecord/test/cases/base_test.rb | 37 +- .../activerecord/test/cases/defaults_test.rb | 31 + .../activerecord/test/cases/dirty_test.rb | 13 + .../activerecord/test/cases/finder_test.rb | 58 +- .../rails/activerecord/test/cases/helper.rb | 14 + .../test/cases/inheritance_test.rb | 2 +- .../activerecord/test/cases/migration_test.rb | 6 +- .../test/cases/reflection_test.rb | 4 +- .../activerecord/test/cases/sanitize_test.rb | 25 + .../test/cases/validations_i18n_test.rb | 26 + .../test/cases/validations_test.rb | 65 +- .../activerecord/test/fixtures/customers.yml | 11 +- .../rails/activerecord/test/models/author.rb | 3 +- .../activerecord/test/models/category.rb | 3 + .../rails/activerecord/test/models/company.rb | 2 + .../activerecord/test/models/customer.rb | 20 +- .../rails/activerecord/test/models/parrot.rb | 1 + vendor/rails/activerecord/test/models/post.rb | 6 - vendor/rails/activesupport/CHANGELOG | 10 + .../activesupport/lib/active_support/cache.rb | 2 +- .../lib/active_support/cache/file_store.rb | 2 +- .../lib/active_support/cache/memory_store.rb | 50 +- .../cache/synchronized_memory_store.rb | 46 + .../lib/active_support/callbacks.rb | 2 +- .../active_support/core_ext/array/grouping.rb | 12 +- .../core_ext/bigdecimal/conversions.rb | 40 +- .../core_ext/date/calculations.rb | 33 +- .../core_ext/date_time/calculations.rb | 24 +- .../lib/active_support/core_ext/enumerable.rb | 4 +- .../active_support/core_ext/file/atomic.rb | 2 +- .../core_ext/hash/conversions.rb | 2 +- .../active_support/core_ext/hash/except.rb | 2 +- .../core_ext/module/synchronization.rb | 9 +- .../lib/active_support/core_ext/rexml.rb | 3 +- .../lib/active_support/core_ext/string.rb | 2 + .../core_ext/string/behavior.rb | 13 + .../core_ext/string/inflections.rb | 19 + .../core_ext/time/calculations.rb | 35 +- .../lib/active_support/inflector.rb | 19 + .../lib/active_support/memoizable.rb | 2 +- .../lib/active_support/multibyte/chars.rb | 5 + .../lib/active_support/test_case.rb | 8 +- .../lib/active_support/time_with_zone.rb | 94 +- .../lib/active_support/values/time_zone.rb | 2 +- .../tzinfo/definitions/Asia/Colombo.rb | 30 + .../test/core_ext/date_ext_test.rb | 23 + .../test/core_ext/date_time_ext_test.rb | 75 ++ .../test/core_ext/hash_ext_test.rb | 14 + .../test/core_ext/string_ext_test.rb | 6 + .../test/core_ext/time_ext_test.rb | 80 +- .../test/core_ext/time_with_zone_test.rb | 74 +- .../activesupport/test/inflector_test.rb | 6 + .../test/inflector_test_cases.rb | 8 + .../test/multibyte_chars_test.rb | 4 + vendor/rails/railties/CHANGELOG | 2 + vendor/rails/railties/Rakefile | 45 +- vendor/rails/railties/lib/commands/console.rb | 13 + vendor/rails/railties/lib/commands/plugin.rb | 18 +- .../railties/lib/commands/process/spawner.rb | 4 +- .../components/mailer/templates/unit_test.rb | 1 - .../rails/railties/lib/tasks/databases.rake | 36 +- 201 files changed, 5491 insertions(+), 1055 deletions(-) create mode 100644 public/javascripts/controls.js create mode 100644 public/javascripts/dragdrop.js create mode 100644 public/javascripts/effects.js delete mode 100644 vendor/plugins/searchgasm-1.0.2/benchmarks/benchmark.rb delete mode 100644 vendor/plugins/searchgasm-1.0.2/benchmarks/benchmark_helper.rb delete mode 100644 vendor/plugins/searchgasm-1.0.2/benchmarks/profile.rb delete mode 100644 vendor/plugins/searchgasm-1.0.2/lib/searchgasm/active_record/associations.rb delete mode 100644 vendor/plugins/searchgasm-1.0.2/lib/searchgasm/utilities.rb rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/CHANGELOG.rdoc (55%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/MIT-LICENSE (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/Manifest (94%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/README.rdoc (87%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/Rakefile (67%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/examples/README.rdoc (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/init.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm.rb (94%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/active_record.rb (100%) create mode 100644 vendor/plugins/searchgasm-1.1.0/lib/searchgasm/active_record/associations.rb rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/active_record/base.rb (76%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/base.rb (99%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/begins_with.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/child_of.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/contains.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/descendant_of.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/does_not_equal.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/ends_with.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/equals.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/greater_than.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/greater_than_or_equal_to.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/inclusive_descendant_of.rb (89%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/keywords.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/less_than.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/less_than_or_equal_to.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/sibling_of.rb (93%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/condition/tree.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/conditions/base.rb (81%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/conditions/protection.rb (66%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/config.rb (55%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/core_ext/hash.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/helpers.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/helpers/control_types.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/helpers/control_types/link.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/helpers/control_types/links.rb (67%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/helpers/control_types/remote_link.rb (97%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/helpers/control_types/remote_links.rb (90%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/helpers/control_types/remote_select.rb (87%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/helpers/control_types/select.rb (95%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/helpers/form.rb (95%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/helpers/utilities.rb (100%) create mode 100644 vendor/plugins/searchgasm-1.1.0/lib/searchgasm/search.rb rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/search/base.rb (55%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/search/conditions.rb (88%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/search/ordering.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/search/pagination.rb (98%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/search/protection.rb (100%) create mode 100644 vendor/plugins/searchgasm-1.1.0/lib/searchgasm/shared/searching.rb create mode 100644 vendor/plugins/searchgasm-1.1.0/lib/searchgasm/shared/utilities.rb create mode 100644 vendor/plugins/searchgasm-1.1.0/lib/searchgasm/shared/virtual_classes.rb rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/lib/searchgasm/version.rb (98%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/searchgasm.gemspec (91%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/fixtures/accounts.yml (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/fixtures/orders.yml (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/fixtures/users.yml (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/libs/acts_as_tree.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/libs/rexml_fix.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_active_record_associations.rb (63%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_active_record_base.rb (89%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_condition_base.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_condition_types.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_conditions_base.rb (80%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_conditions_protection.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_config.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_helper.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_search_base.rb (90%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_search_conditions.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_search_ordering.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_search_pagination.rb (79%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/test_search_protection.rb (100%) rename vendor/plugins/{searchgasm-1.0.2 => searchgasm-1.1.0}/test/text_config.rb (100%) create mode 100644 vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.erb~ create mode 100644 vendor/rails/actionpack/lib/action_controller/translation.rb create mode 100644 vendor/rails/actionpack/test/controller/translation_test.rb create mode 100644 vendor/rails/activerecord/test/cases/sanitize_test.rb create mode 100644 vendor/rails/activesupport/lib/active_support/cache/synchronized_memory_store.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/string/behavior.rb create mode 100644 vendor/rails/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/definitions/Asia/Colombo.rb diff --git a/README.rdoc b/README.rdoc index 629ed3c..91949f2 100644 --- a/README.rdoc +++ b/README.rdoc @@ -2,10 +2,12 @@ This is an example of how to use Searchgasm in a rails app. Searchgasm is simple searching, ordering, and pagination all in one plugin. -Checkout Searchgasm here: http://github.com/binarylogic/searchgasm +This application live: http://searchgasm_example.binarylogic.com Quick tutorial on how to build this application here: http://www.binarylogic.com/2008/9/7/tutorial-pagination-ordering-and-searching-with-searchgasm +Searchgasm: http://github.com/binarylogic/searchgasm + === Key files Here are the files you want to glance at: diff --git a/app/models/user_group.rb b/app/models/user_group.rb index fd0d24b..c3d4e68 100644 --- a/app/models/user_group.rb +++ b/app/models/user_group.rb @@ -1,3 +1,4 @@ class UserGroup < ActiveRecord::Base has_many :users + has_many :orders, :through => :users end diff --git a/app/views/jquery/users/index.html.erb b/app/views/jquery/users/index.html.erb index 3b5e1ab..15dc302 100644 --- a/app/views/jquery/users/index.html.erb +++ b/app/views/jquery/users/index.html.erb @@ -7,6 +7,10 @@ This is the preferred method. Really nice, check out jquery_setup.js on how to s Search Users <% f.fields_for @search.conditions do |users| %> + Match ANY or ALL of the conditions:
+ <%= users.select :any, [["All", false], ["Any", true]] %>
+
+ First name keywords:
<%= users.text_field :first_name_keywords %>

diff --git a/app/views/non_ajax/users/index.html.erb b/app/views/non_ajax/users/index.html.erb index 6c87627..c4f32d7 100644 --- a/app/views/non_ajax/users/index.html.erb +++ b/app/views/non_ajax/users/index.html.erb @@ -7,6 +7,10 @@ Good ol' regular http requests. Search Users <% f.fields_for @search.conditions do |users| %> + Match ANY or ALL of the conditions:
+ <%= users.select :any, [["All", false], ["Any", true]] %>
+
+ First name keywords:
<%= users.text_field :first_name_keywords %>

diff --git a/app/views/rails_ajax/users/index.html.erb b/app/views/rails_ajax/users/index.html.erb index d70b997..cfc47dd 100644 --- a/app/views/rails_ajax/users/index.html.erb +++ b/app/views/rails_ajax/users/index.html.erb @@ -9,10 +9,13 @@ If you are wanting to do unobtrusive javascript take a look at the jQuery exampl Search Users <% f.fields_for @search.conditions do |users| %> + Match ANY or ALL of the conditions:
+ <%= users.select :any, [["All", false], ["Any", true]] %>
+
+ First name keywords:
<%= users.text_field :first_name_keywords %>

- First name sounds like: (try "bin", this is a custom condition, checkout config/initializers/searchgasm.rb)
<%= users.text_field :first_name_sounds_like %>

@@ -24,13 +27,11 @@ If you are wanting to do unobtrusive javascript take a look at the jQuery exampl Email ends with:
<%= users.text_field :email_ends_with %>

- <% users.fields_for users.object.orders do |orders| %> Has orders with a total greater than:
$<%= orders.text_field :total_gt %>

<% end %> - <% users.fields_for users.object.user_group do |user_group| %> Belongs to user group with name that starts with:
<%= user_group.text_field :name_starts_with %>
@@ -43,4 +44,4 @@ If you are wanting to do unobtrusive javascript take a look at the jQuery exampl
<%= render :partial => "users" %> -
+ \ No newline at end of file diff --git a/config/boot.rb b/config/boot.rb index cd21fb9..6a30b54 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -82,14 +82,14 @@ def gem_version def load_rubygems require 'rubygems' - - unless rubygems_version >= '0.9.4' - $stderr.puts %(Rails requires RubyGems >= 0.9.4 (you have #{rubygems_version}). Please `gem update --system` and try again.) + min_version = '1.1.1' + unless rubygems_version >= min_version + $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.) exit 1 end rescue LoadError - $stderr.puts %(Rails requires RubyGems >= 0.9.4. Please install RubyGems and try again: http://rubygems.rubyforge.org) + $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org) exit 1 end diff --git a/config/environment.rb b/config/environment.rb index 3162af1..01f1123 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -5,7 +5,7 @@ # ENV['RAILS_ENV'] ||= 'production' # Specifies gem version of Rails to use when vendor/rails is not present -RAILS_GEM_VERSION = '2.1.0' unless defined? RAILS_GEM_VERSION +RAILS_GEM_VERSION = '2.1.1' unless defined? RAILS_GEM_VERSION # Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') diff --git a/public/javascripts/controls.js b/public/javascripts/controls.js new file mode 100644 index 0000000..5aaf0bb --- /dev/null +++ b/public/javascripts/controls.js @@ -0,0 +1,963 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005-2007 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +if(typeof Effect == 'undefined') + throw("controls.js requires including script.aculo.us' effects.js library"); + +var Autocompleter = { } +Autocompleter.Base = Class.create({ + baseInitialize: function(element, update, options) { + element = $(element) + this.element = element; + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + this.oldElementValue = this.element.value; + + if(this.setOptions) + this.setOptions(options); + else + this.options = options || { }; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, { + setHeight: false, + offsetTop: element.offsetHeight + }); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if(typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + // Force carriage returns as token delimiters anyway + if (!this.options.tokens.include('\n')) + this.options.tokens.push('\n'); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (Prototype.Browser.IE) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + ''); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + + markPrevious: function() { + if(this.index > 0) this.index-- + else this.index = this.entryCount-1; + this.getEntry(this.index).scrollIntoView(true); + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++ + else this.index = 0; + this.getEntry(this.index).scrollIntoView(false); + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = $(selectedElement).select('.' + this.options.select) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var bounds = this.getTokenBounds(); + if (bounds[0] != -1) { + var newValue = this.element.value.substr(0, bounds[0]); + var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value + this.element.value.substr(bounds[1]); + } else { + this.element.value = value; + } + this.oldElementValue = this.element.value; + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.down()); + + if(this.update.firstChild && this.update.down().childNodes) { + this.entryCount = + this.update.down().childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + this.index = 0; + + if(this.entryCount==1 && this.options.autoSelect) { + this.selectEntry(); + this.hide(); + } else { + this.render(); + } + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + this.tokenBounds = null; + if(this.getToken().length>=this.options.minChars) { + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + this.oldElementValue = this.element.value; + }, + + getToken: function() { + var bounds = this.getTokenBounds(); + return this.element.value.substring(bounds[0], bounds[1]).strip(); + }, + + getTokenBounds: function() { + if (null != this.tokenBounds) return this.tokenBounds; + var value = this.element.value; + if (value.strip().empty()) return [-1, 0]; + var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); + var offset = (diff == this.oldElementValue.length ? 1 : 0); + var prevTokenPos = -1, nextTokenPos = value.length; + var tp; + for (var index = 0, l = this.options.tokens.length; index < l; ++index) { + tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); + if (tp > prevTokenPos) prevTokenPos = tp; + tp = value.indexOf(this.options.tokens[index], diff + offset); + if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; + } + return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); + } +}); + +Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { + var boundary = Math.min(newS.length, oldS.length); + for (var index = 0; index < boundary; ++index) + if (newS[index] != oldS[index]) + return index; + return boundary; +}; + +Ajax.Autocompleter = Class.create(Autocompleter.Base, { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + this.startIndicator(); + + var entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(Autocompleter.Base, { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("
  • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
  • "); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("
  • " + elem.substr(0, foundPos) + "" + + elem.substr(foundPos, entry.length) + "" + elem.substr( + foundPos + entry.length) + "
  • "); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + return ""; + } + }, options || { }); + } +}); + +// AJAX in-place editor and collection editor +// Full rewrite by Christophe Porteneuve (April 2007). + +// Use this if you notice weird scrolling problems on some browsers, +// the DOM might be a bit confused when this gets called so do this +// waits 1 ms (with setTimeout) until it does the activation +Field.scrollFreeActivate = function(field) { + setTimeout(function() { + Field.activate(field); + }, 1); +} + +Ajax.InPlaceEditor = Class.create({ + initialize: function(element, url, options) { + this.url = url; + this.element = element = $(element); + this.prepareOptions(); + this._controls = { }; + arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! + Object.extend(this.options, options || { }); + if (!this.options.formId && this.element.id) { + this.options.formId = this.element.id + '-inplaceeditor'; + if ($(this.options.formId)) + this.options.formId = ''; + } + if (this.options.externalControl) + this.options.externalControl = $(this.options.externalControl); + if (!this.options.externalControl) + this.options.externalControlOnly = false; + this._originalBackground = this.element.getStyle('background-color') || 'transparent'; + this.element.title = this.options.clickToEditText; + this._boundCancelHandler = this.handleFormCancellation.bind(this); + this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); + this._boundFailureHandler = this.handleAJAXFailure.bind(this); + this._boundSubmitHandler = this.handleFormSubmission.bind(this); + this._boundWrapperHandler = this.wrapUp.bind(this); + this.registerListeners(); + }, + checkForEscapeOrReturn: function(e) { + if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; + if (Event.KEY_ESC == e.keyCode) + this.handleFormCancellation(e); + else if (Event.KEY_RETURN == e.keyCode) + this.handleFormSubmission(e); + }, + createControl: function(mode, handler, extraClasses) { + var control = this.options[mode + 'Control']; + var text = this.options[mode + 'Text']; + if ('button' == control) { + var btn = document.createElement('input'); + btn.type = 'submit'; + btn.value = text; + btn.className = 'editor_' + mode + '_button'; + if ('cancel' == mode) + btn.onclick = this._boundCancelHandler; + this._form.appendChild(btn); + this._controls[mode] = btn; + } else if ('link' == control) { + var link = document.createElement('a'); + link.href = '#'; + link.appendChild(document.createTextNode(text)); + link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; + link.className = 'editor_' + mode + '_link'; + if (extraClasses) + link.className += ' ' + extraClasses; + this._form.appendChild(link); + this._controls[mode] = link; + } + }, + createEditField: function() { + var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); + var fld; + if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { + fld = document.createElement('input'); + fld.type = 'text'; + var size = this.options.size || this.options.cols || 0; + if (0 < size) fld.size = size; + } else { + fld = document.createElement('textarea'); + fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); + fld.cols = this.options.cols || 40; + } + fld.name = this.options.paramName; + fld.value = text; // No HTML breaks conversion anymore + fld.className = 'editor_field'; + if (this.options.submitOnBlur) + fld.onblur = this._boundSubmitHandler; + this._controls.editor = fld; + if (this.options.loadTextURL) + this.loadExternalText(); + this._form.appendChild(this._controls.editor); + }, + createForm: function() { + var ipe = this; + function addText(mode, condition) { + var text = ipe.options['text' + mode + 'Controls']; + if (!text || condition === false) return; + ipe._form.appendChild(document.createTextNode(text)); + }; + this._form = $(document.createElement('form')); + this._form.id = this.options.formId; + this._form.addClassName(this.options.formClassName); + this._form.onsubmit = this._boundSubmitHandler; + this.createEditField(); + if ('textarea' == this._controls.editor.tagName.toLowerCase()) + this._form.appendChild(document.createElement('br')); + if (this.options.onFormCustomization) + this.options.onFormCustomization(this, this._form); + addText('Before', this.options.okControl || this.options.cancelControl); + this.createControl('ok', this._boundSubmitHandler); + addText('Between', this.options.okControl && this.options.cancelControl); + this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); + addText('After', this.options.okControl || this.options.cancelControl); + }, + destroy: function() { + if (this._oldInnerHTML) + this.element.innerHTML = this._oldInnerHTML; + this.leaveEditMode(); + this.unregisterListeners(); + }, + enterEditMode: function(e) { + if (this._saving || this._editing) return; + this._editing = true; + this.triggerCallback('onEnterEditMode'); + if (this.options.externalControl) + this.options.externalControl.hide(); + this.element.hide(); + this.createForm(); + this.element.parentNode.insertBefore(this._form, this.element); + if (!this.options.loadTextURL) + this.postProcessEditField(); + if (e) Event.stop(e); + }, + enterHover: function(e) { + if (this.options.hoverClassName) + this.element.addClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onEnterHover'); + }, + getText: function() { + return this.element.innerHTML; + }, + handleAJAXFailure: function(transport) { + this.triggerCallback('onFailure', transport); + if (this._oldInnerHTML) { + this.element.innerHTML = this._oldInnerHTML; + this._oldInnerHTML = null; + } + }, + handleFormCancellation: function(e) { + this.wrapUp(); + if (e) Event.stop(e); + }, + handleFormSubmission: function(e) { + var form = this._form; + var value = $F(this._controls.editor); + this.prepareSubmission(); + var params = this.options.callback(form, value) || ''; + if (Object.isString(params)) + params = params.toQueryParams(); + params.editorId = this.element.id; + if (this.options.htmlResponse) { + var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Updater({ success: this.element }, this.url, options); + } else { + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.url, options); + } + if (e) Event.stop(e); + }, + leaveEditMode: function() { + this.element.removeClassName(this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + if (this.options.externalControl) + this.options.externalControl.show(); + this._saving = false; + this._editing = false; + this._oldInnerHTML = null; + this.triggerCallback('onLeaveEditMode'); + }, + leaveHover: function(e) { + if (this.options.hoverClassName) + this.element.removeClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onLeaveHover'); + }, + loadExternalText: function() { + this._form.addClassName(this.options.loadingClassName); + this._controls.editor.disabled = true; + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._form.removeClassName(this.options.loadingClassName); + var text = transport.responseText; + if (this.options.stripLoadedTextTags) + text = text.stripTags(); + this._controls.editor.value = text; + this._controls.editor.disabled = false; + this.postProcessEditField(); + }.bind(this), + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + postProcessEditField: function() { + var fpc = this.options.fieldPostCreation; + if (fpc) + $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); + }, + prepareOptions: function() { + this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); + Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); + [this._extraDefaultOptions].flatten().compact().each(function(defs) { + Object.extend(this.options, defs); + }.bind(this)); + }, + prepareSubmission: function() { + this._saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + registerListeners: function() { + this._listeners = { }; + var listener; + $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { + listener = this[pair.value].bind(this); + this._listeners[pair.key] = listener; + if (!this.options.externalControlOnly) + this.element.observe(pair.key, listener); + if (this.options.externalControl) + this.options.externalControl.observe(pair.key, listener); + }.bind(this)); + }, + removeForm: function() { + if (!this._form) return; + this._form.remove(); + this._form = null; + this._controls = { }; + }, + showSaving: function() { + this._oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + this.element.addClassName(this.options.savingClassName); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + }, + triggerCallback: function(cbName, arg) { + if ('function' == typeof this.options[cbName]) { + this.options[cbName](this, arg); + } + }, + unregisterListeners: function() { + $H(this._listeners).each(function(pair) { + if (!this.options.externalControlOnly) + this.element.stopObserving(pair.key, pair.value); + if (this.options.externalControl) + this.options.externalControl.stopObserving(pair.key, pair.value); + }.bind(this)); + }, + wrapUp: function(transport) { + this.leaveEditMode(); + // Can't use triggerCallback due to backward compatibility: requires + // binding + direct element + this._boundComplete(transport, this.element); + } +}); + +Object.extend(Ajax.InPlaceEditor.prototype, { + dispose: Ajax.InPlaceEditor.prototype.destroy +}); + +Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { + initialize: function($super, element, url, options) { + this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; + $super(element, url, options); + }, + + createEditField: function() { + var list = document.createElement('select'); + list.name = this.options.paramName; + list.size = 1; + this._controls.editor = list; + this._collection = this.options.collection || []; + if (this.options.loadCollectionURL) + this.loadCollection(); + else + this.checkForExternalText(); + this._form.appendChild(this._controls.editor); + }, + + loadCollection: function() { + this._form.addClassName(this.options.loadingClassName); + this.showLoadingText(this.options.loadingCollectionText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + var js = transport.responseText.strip(); + if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check + throw 'Server returned an invalid collection representation.'; + this._collection = eval(js); + this.checkForExternalText(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadCollectionURL, options); + }, + + showLoadingText: function(text) { + this._controls.editor.disabled = true; + var tempOption = this._controls.editor.firstChild; + if (!tempOption) { + tempOption = document.createElement('option'); + tempOption.value = ''; + this._controls.editor.appendChild(tempOption); + tempOption.selected = true; + } + tempOption.update((text || '').stripScripts().stripTags()); + }, + + checkForExternalText: function() { + this._text = this.getText(); + if (this.options.loadTextURL) + this.loadExternalText(); + else + this.buildOptionList(); + }, + + loadExternalText: function() { + this.showLoadingText(this.options.loadingText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._text = transport.responseText.strip(); + this.buildOptionList(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + + buildOptionList: function() { + this._form.removeClassName(this.options.loadingClassName); + this._collection = this._collection.map(function(entry) { + return 2 === entry.length ? entry : [entry, entry].flatten(); + }); + var marker = ('value' in this.options) ? this.options.value : this._text; + var textFound = this._collection.any(function(entry) { + return entry[0] == marker; + }.bind(this)); + this._controls.editor.update(''); + var option; + this._collection.each(function(entry, index) { + option = document.createElement('option'); + option.value = entry[0]; + option.selected = textFound ? entry[0] == marker : 0 == index; + option.appendChild(document.createTextNode(entry[1])); + this._controls.editor.appendChild(option); + }.bind(this)); + this._controls.editor.disabled = false; + Field.scrollFreeActivate(this._controls.editor); + } +}); + +//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** +//**** This only exists for a while, in order to let **** +//**** users adapt to the new API. Read up on the new **** +//**** API and convert your code to it ASAP! **** + +Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { + if (!options) return; + function fallback(name, expr) { + if (name in options || expr === undefined) return; + options[name] = expr; + }; + fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : + options.cancelLink == options.cancelButton == false ? false : undefined))); + fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : + options.okLink == options.okButton == false ? false : undefined))); + fallback('highlightColor', options.highlightcolor); + fallback('highlightEndColor', options.highlightendcolor); +}; + +Object.extend(Ajax.InPlaceEditor, { + DefaultOptions: { + ajaxOptions: { }, + autoRows: 3, // Use when multi-line w/ rows == 1 + cancelControl: 'link', // 'link'|'button'|false + cancelText: 'cancel', + clickToEditText: 'Click to edit', + externalControl: null, // id|elt + externalControlOnly: false, + fieldPostCreation: 'activate', // 'activate'|'focus'|false + formClassName: 'inplaceeditor-form', + formId: null, // id|elt + highlightColor: '#ffff99', + highlightEndColor: '#ffffff', + hoverClassName: '', + htmlResponse: true, + loadingClassName: 'inplaceeditor-loading', + loadingText: 'Loading...', + okControl: 'button', // 'link'|'button'|false + okText: 'ok', + paramName: 'value', + rows: 1, // If 1 and multi-line, uses autoRows + savingClassName: 'inplaceeditor-saving', + savingText: 'Saving...', + size: 0, + stripLoadedTextTags: false, + submitOnBlur: false, + textAfterControls: '', + textBeforeControls: '', + textBetweenControls: '' + }, + DefaultCallbacks: { + callback: function(form) { + return Form.serialize(form); + }, + onComplete: function(transport, element) { + // For backward compatibility, this one is bound to the IPE, and passes + // the element directly. It was too often customized, so we don't break it. + new Effect.Highlight(element, { + startcolor: this.options.highlightColor, keepBackgroundImage: true }); + }, + onEnterEditMode: null, + onEnterHover: function(ipe) { + ipe.element.style.backgroundColor = ipe.options.highlightColor; + if (ipe._effect) + ipe._effect.cancel(); + }, + onFailure: function(transport, ipe) { + alert('Error communication with the server: ' + transport.responseText.stripTags()); + }, + onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. + onLeaveEditMode: null, + onLeaveHover: function(ipe) { + ipe._effect = new Effect.Highlight(ipe.element, { + startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, + restorecolor: ipe._originalBackground, keepBackgroundImage: true + }); + } + }, + Listeners: { + click: 'enterEditMode', + keydown: 'checkForEscapeOrReturn', + mouseover: 'enterHover', + mouseout: 'leaveHover' + } +}); + +Ajax.InPlaceCollectionEditor.DefaultOptions = { + loadingCollectionText: 'Loading options...' +}; + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create({ + initialize: function(element, delay, callback) { + this.delay = delay || 0.5; + this.element = $(element); + this.callback = callback; + this.timer = null; + this.lastValue = $F(this.element); + Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); + }, + delayedListener: function(event) { + if(this.lastValue == $F(this.element)) return; + if(this.timer) clearTimeout(this.timer); + this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); + this.lastValue = $F(this.element); + }, + onTimerEvent: function() { + this.timer = null; + this.callback(this.element, $F(this.element)); + } +}); diff --git a/public/javascripts/dragdrop.js b/public/javascripts/dragdrop.js new file mode 100644 index 0000000..bf5cfea --- /dev/null +++ b/public/javascripts/dragdrop.js @@ -0,0 +1,972 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +if(Object.isUndefined(Effect)) + throw("dragdrop.js requires including script.aculo.us' effects.js library"); + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==$(element) }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null, + tree: false + }, arguments[1] || { }); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if(Object.isArray(containment)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + if(options.accept) options.accept = [options.accept].flatten(); + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + + isContained: function(element, drop) { + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); + }, + + isAffected: function(point, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.classNames(element).detect( + function(v) { return drop.accept.include(v) } ) )) && + Position.within(drop.element, point[0], point[1]) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.removeClassName(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(drop.hoverclass) + Element.addClassName(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(point, element) { + if(!this.drops.length) return; + var drop, affected = []; + + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) + drop = Droppables.findDeepestChild(affected); + + if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); + if (drop) { + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + if (drop != this.last_active) Droppables.activate(drop); + } + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) + if (this.last_active.onDrop) { + this.last_active.onDrop(element, this.last_active.element, event); + return true; + } + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +} + +var Draggables = { + drags: [], + observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + if(draggable.options.delay) { + this._timeout = setTimeout(function() { + Draggables._timeout = null; + window.focus(); + Draggables.activeDraggable = draggable; + }.bind(this), draggable.options.delay); + } else { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + } + }, + + deactivate: function() { + this.activeDraggable = null; + }, + + updateDrag: function(event) { + if(!this.activeDraggable) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; + this._lastPointer = pointer; + + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function(event) { + if(this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + + addObserver: function(observer) { + this.observers.push(observer); + this._cacheObserverCallbacks(); + }, + + removeObserver: function(element) { // element instead of observer fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + this._cacheObserverCallbacks(); + }, + + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' + if(this[eventName+'Count'] > 0) + this.observers.each( function(o) { + if(o[eventName]) o[eventName](eventName, draggable, event); + }); + if(draggable.options[eventName]) draggable.options[eventName](draggable, event); + }, + + _cacheObserverCallbacks: function() { + ['onStart','onEnd','onDrag'].each( function(eventName) { + Draggables[eventName+'Count'] = Draggables.observers.select( + function(o) { return o[eventName]; } + ).length; + }); + } +} + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create({ + initialize: function(element) { + var defaults = { + handle: false, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, + queue: {scope:'_draggable', position:'end'} + }); + }, + endeffect: function(element) { + var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, + queue: {scope:'_draggable', position:'end'}, + afterFinish: function(){ + Draggable._dragging[element] = false + } + }); + }, + zindex: 1000, + revert: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } + delay: 0 + }; + + if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) + Object.extend(defaults, { + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + Draggable._dragging[element] = true; + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + } + }); + + var options = Object.extend(defaults, arguments[1] || { }); + + this.element = $(element); + + if(options.handle && Object.isString(options.handle)) + this.handle = this.element.down('.'+options.handle, 0); + + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { + options.scroll = $(options.scroll); + this._isScrollChild = Element.childOf(this.element, options.scroll); + } + + Element.makePositioned(this.element); // fix IE + + this.options = options; + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Draggables.unregister(this); + }, + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(!Object.isUndefined(Draggable._dragging[this.element]) && + Draggable._dragging[this.element]) return; + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if((tag_name = src.tagName.toUpperCase()) && ( + tag_name=='INPUT' || + tag_name=='SELECT' || + tag_name=='OPTION' || + tag_name=='BUTTON' || + tag_name=='TEXTAREA')) return; + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = Position.cumulativeOffset(this.element); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); + + Draggables.activate(this); + Event.stop(event); + } + }, + + startDrag: function(event) { + this.dragging = true; + if(!this.delta) + this.delta = this.currentDelta(); + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); + if (!this.element._originallyAbsolute) + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + + if(!this.options.quiet){ + Position.prepare(); + Droppables.show(pointer, this.element); + } + + Draggables.notify('onDrag', this, event); + + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft + Position.deltaX; + p[1] += this.options.scroll.scrollTop + Position.deltaY; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(Prototype.Browser.WebKit) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.quiet){ + Position.prepare(); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + Droppables.show(pointer, this.element); + } + + if(this.options.ghosting) { + if (!this.element._originallyAbsolute) + Position.relativize(this.element); + delete this.element._originallyAbsolute; + Element.remove(this._clone); + this._clone = null; + } + + var dropped = false; + if(success) { + dropped = Droppables.fire(event, this.element); + if (!dropped) dropped = false; + } + if(dropped && this.options.onDropped) this.options.onDropped(this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && Object.isFunction(revert)) revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + if (dropped == 0 || revert != 'failure') + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); + } else { + this.delta = d; + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + Draggables.deactivate(this); + Droppables.reset(); + }, + + keyPress: function(event) { + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); + }, + + endDrag: function(event) { + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + if(this.options.ghosting) { + var r = Position.realOffset(this.element); + pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; + } + + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(Object.isFunction(this.options.snap)) { + p = this.options.snap(p[0],p[1],this); + } else { + if(Object.isArray(this.options.snap)) { + p = p.map( function(v, i) { + return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)) + } else { + p = p.map( function(v) { + return (v/this.options.snap).round()*this.options.snap }.bind(this)) + } + }} + + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = p[0] + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = p[1] + "px"; + + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + if(!(speed[0] || speed[1])) return; + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + if (this._isScrollChild) { + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + } + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight + } + } + return { top: T, left: L, width: W, height: H }; + } +}); + +Draggable._dragging = { }; + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create({ + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +}); + +var Sortable = { + SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, + + sortables: { }, + + _findRootElement: function(element) { + while (element.tagName.toUpperCase() != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + + destroy: function(element){ + var s = Sortable.options(element); + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, + treeTag: 'ul', + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + delay: 0, + hoverclass: null, + ghosting: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: this.SERIALIZE_RULE, + + // these take arrays of elements or ids and can be + // used for better initialization performance + elements: false, + handles: false, + + onChange: Prototype.emptyFunction, + onUpdate: Prototype.emptyFunction + }, arguments[1] || { }); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + quiet: options.quiet, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + delay: options.delay, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + } + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (options.elements || this.findElements(element, options) || []).each( function(e,i) { + var handle = options.handles ? $(options.handles[i]) : + (options.handle ? $(e).select('.' + options.handle)[0] : e); + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.droppables.push(e); + }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + + // keep reference + this.sortables[element.id] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + onHover: function(element, dropon, overlap) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + Sortable.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Sortable._marker.hide(); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = + ($('dropmarker') || Element.extend(document.createElement('DIV'))). + hide().addClassName('dropmarker').setStyle({position:'absolute'}); + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); + else + Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); + + Sortable._marker.show(); + }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: [], + position: parent.children.length, + container: $(children[i]).down(options.treeTag) + } + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child) + + parent.children.push (child); + } + + return parent; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || { }); + + var root = { + id: null, + parent: null, + children: [], + container: element, + position: 0 + } + + return Sortable._tree(element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || { }); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || { }); + + var nodeMap = { }; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || { }); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "[id]=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } + } +} + +// Returns true if child is contained within element +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + if (child.parentNode == element) return true; + return Element.isParent(child.parentNode, element); +} + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +} + +Element.offsetSize = function (element, type) { + return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; +} diff --git a/public/javascripts/effects.js b/public/javascripts/effects.js new file mode 100644 index 0000000..f030b5d --- /dev/null +++ b/public/javascripts/effects.js @@ -0,0 +1,1120 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if (this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if (this.slice(0,1) == '#') { + if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if (this.length==7) color = this.toLowerCase(); + } + } + return (color.length==7 ? color : (arguments[0] || this)); +}; + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +}; + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +}; + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.setStyle({fontSize: (percent/100) + 'em'}); + if (Prototype.Browser.WebKit) window.scrollBy(0,0); + return element; +}; + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +}; + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, + Transitions: { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; + return pos > 1 ? 1 : pos; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; + }, + pulse: function(pos, pulses) { + pulses = pulses || 5; + return ( + ((pos % (1/pulses)) * pulses).round() == 0 ? + ((pos * pulses * 2) - (pos * pulses * 2).floor()) : + 1 - ((pos * pulses * 2) - (pos * pulses * 2).floor()) + ); + }, + spring: function(pos) { + return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } + }, + DefaultOptions: { + duration: 1.0, // seconds + fps: 100, // 100= assume 66fps max. + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' + }, + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; + + element = $(element); + $A(element.childNodes).each( function(child) { + if (child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + new Element('span', {style: tagifyStyle}).update( + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if (((typeof element == 'object') || + Object.isFunction(element)) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || { }); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || { }); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(Enumerable, { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = Object.isString(effect.options.queue) ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'with-last': + timestamp = this.effects.pluck('startOn').max() || timestamp; + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if (!this.interval) + this.interval = setInterval(this.loop.bind(this), 15); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if (this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + for(var i=0, len=this.effects.length;i= this.startOn) { + if (timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if (this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / this.totalTime, + frame = (pos * this.totalFrames).round(); + if (frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + cancel: function() { + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if (this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + var data = $H(); + for(property in this) + if (!Object.isFunction(this[property])) data.set(property, this[property]); + return '#'; + } +}); + +Effect.Parallel = Class.create(Effect.Base, { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if (effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Tween = Class.create(Effect.Base, { + initialize: function(object, from, to) { + object = Object.isString(object) ? $(object) : object; + var args = $A(arguments), method = args.last(), + options = args.length == 5 ? args[3] : null; + this.method = Object.isFunction(method) ? method.bind(object) : + Object.isFunction(object[method]) ? object[method].bind(object) : + function(value) { object[method] = value }; + this.start(Object.extend({ from: from, to: to }, options || { })); + }, + update: function(position) { + this.method(position); + } +}); + +Effect.Event = Class.create(Effect.Base, { + initialize: function() { + this.start(Object.extend({ duration: 0 }, arguments[0] || { })); + }, + update: Prototype.emptyFunction +}); + +Effect.Opacity = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + // make this work on IE on elements without 'layout' + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || { }); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if (this.options.mode == 'absolute') { + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: (this.options.x * position + this.originalLeft).round() + 'px', + top: (this.options.y * position + this.originalTop).round() + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); +}; + +Effect.Scale = Class.create(Effect.Base, { + initialize: function(element, percent) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or { } with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || { }); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = { }; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%','pt'].each( function(fontSizeType) { + if (fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if (this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if (/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if (!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if (this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = { }; + if (this.options.scaleX) d.width = width.round() + 'px'; + if (this.options.scaleY) d.height = height.round() + 'px'; + if (this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if (this.elementPositioning == 'absolute') { + if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if (this.options.scaleY) d.top = -topd + 'px'; + if (this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if (this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { }; + if (!this.options.keepBackgroundImage) { + this.oldStyle.backgroundImage = this.element.getStyle('background-image'); + this.element.setStyle({backgroundImage: 'none'}); + } + if (!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if (!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); + }, + update: function(position) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = function(element) { + var options = arguments[1] || { }, + scrollOffsets = document.viewport.getScrollOffsets(), + elementOffsets = $(element).cumulativeOffset(), + max = (window.height || document.body.scrollHeight) - document.viewport.getHeight(); + + if (options.offset) elementOffsets[1] += options.offset; + + return new Effect.Tween(null, + scrollOffsets.top, + elementOffsets[1] > max ? max : elementOffsets[1], + options, + function(p){ scrollTo(scrollOffsets.left, p.round()) } + ); +}; + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if (effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + } + }, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from).show(); + }}, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { + opacity: element.getInlineOpacity(), + position: element.getStyle('position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + Position.absolutize(effect.effects[0].element) + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().setStyle(oldStyle); } + }, arguments[1] || { }) + ); +}; + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }, arguments[1] || { }) + ); +}; + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || { })); +}; + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, Object.extend({ + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); + } + }) + } + }, arguments[1] || { })); +}; + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); + } + }, arguments[1] || { })); +}; + +Effect.Shake = function(element) { + element = $(element); + var options = Object.extend({ + distance: 20, + duration: 0.5 + }, arguments[1] || {}); + var distance = parseFloat(options.distance); + var split = parseFloat(options.duration) / 10.0; + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { + effect.element.undoPositioned().setStyle(oldStyle); + }}) }}) }}) }}) }}) }}); +}; + +Effect.SlideDown = function(element) { + element = $(element).cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || { }) + ); +}; + +Effect.SlideUp = function(element) { + element = $(element).cleanWhitespace(); + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); + } + }, arguments[1] || { }) + ); +}; + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, { + restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }); +}; + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || { }); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide().makeClipping().makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); + } + }, options) + ) + } + }); +}; + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || { }); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } + }, options) + ); +}; + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || { }; + var oldOpacity = element.getInlineOpacity(); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 2.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +}; + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + element.makeClipping(); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().setStyle(oldStyle); + } }); + }}, arguments[1] || { })); +}; + +Effect.Morph = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: { } + }, arguments[1] || { }); + + if (!Object.isString(options.style)) this.style = $H(options.style); + else { + if (options.style.include(':')) + this.style = options.style.parseStyle(); + else { + this.element.addClassName(options.style); + this.style = $H(this.element.getStyles()); + this.element.removeClassName(options.style); + var css = this.element.getStyles(); + this.style = this.style.reject(function(style) { + return style.value == css[style.key]; + }); + options.afterFinishInternal = function(effect) { + effect.element.addClassName(effect.options.style); + effect.transforms.each(function(transform) { + effect.element.style[transform.style] = ''; + }); + } + } + } + this.start(options); + }, + + setup: function(){ + function parseColor(color){ + if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + color = color.parseColor(); + return $R(0,2).map(function(i){ + return parseInt( color.slice(i*2+1,i*2+3), 16 ) + }); + } + this.transforms = this.style.map(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if (value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if (property == 'opacity') { + value = parseFloat(value); + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + } else if (Element.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + } + + var originalValue = this.element.getStyle(property); + return { + style: property.camelize(), + originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: unit=='color' ? parseColor(value) : value, + unit: unit + }; + }.bind(this)).reject(function(transform){ + return ( + (transform.originalValue == transform.targetValue) || + ( + transform.unit != 'color' && + (isNaN(transform.originalValue) || isNaN(transform.targetValue)) + ) + ) + }); + }, + update: function(position) { + var style = { }, transform, i = this.transforms.length; + while(i--) + style[(transform = this.transforms[i]).style] = + transform.unit=='color' ? '#'+ + (Math.round(transform.originalValue[0]+ + (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + + (Math.round(transform.originalValue[1]+ + (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + + (Math.round(transform.originalValue[2]+ + (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : + (transform.originalValue + + (transform.targetValue - transform.originalValue) * position).toFixed(3) + + (transform.unit === null ? '' : transform.unit); + this.element.setStyle(style, true); + } +}); + +Effect.Transform = Class.create({ + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || { }; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + track = $H(track); + var data = track.values().first(); + this.tracks.push($H({ + ids: track.keys().first(), + effect: Effect.Morph, + options: { style: data } + })); + }.bind(this)); + return this; + }, + play: function(){ + return new Effect.Parallel( + this.tracks.map(function(track){ + var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); + var elements = [$(ids) || $$(ids)].flatten(); + return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = $w( + 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + + 'fontSize fontWeight height left letterSpacing lineHeight ' + + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ + 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + + 'right textIndent top width wordSpacing zIndex'); + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.__parseStyleElement = document.createElement('div'); +String.prototype.parseStyle = function(){ + var style, styleRules = $H(); + if (Prototype.Browser.WebKit) + style = new Element('div',{style:this}).style; + else { + String.__parseStyleElement.innerHTML = '
    '; + style = String.__parseStyleElement.childNodes[0].style; + } + + Element.CSS_PROPERTIES.each(function(property){ + if (style[property]) styleRules.set(property, style[property]); + }); + + if (Prototype.Browser.IE && this.include('opacity')) + styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); + + return styleRules; +}; + +if (document.defaultView && document.defaultView.getComputedStyle) { + Element.getStyles = function(element) { + var css = document.defaultView.getComputedStyle($(element), null); + return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { + styles[property] = css[property]; + return styles; + }); + }; +} else { + Element.getStyles = function(element) { + element = $(element); + var css = element.currentStyle, styles; + styles = Element.CSS_PROPERTIES.inject({ }, function(hash, property) { + hash.set(property, css[property]); + return hash; + }); + if (!styles.opacity) styles.set('opacity', element.getOpacity()); + return styles; + }; +}; + +Effect.Methods = { + morph: function(element, style) { + element = $(element); + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); + return element; + }, + visualEffect: function(element, effect, options) { + element = $(element) + var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[klass](element, options); + return element; + }, + highlight: function(element, options) { + element = $(element); + new Effect.Highlight(element, options); + return element; + } +}; + +$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ + 'pulsate shake puff squish switchOff dropOut').each( + function(effect) { + Effect.Methods[effect] = function(element, options){ + element = $(element); + Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); + return element; + } + } +); + +$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( + function(f) { Effect.Methods[f] = Element[f]; } +); + +Element.addMethods(Effect.Methods); diff --git a/vendor/plugins/searchgasm-1.0.2/benchmarks/benchmark.rb b/vendor/plugins/searchgasm-1.0.2/benchmarks/benchmark.rb deleted file mode 100644 index fcdd4f6..0000000 --- a/vendor/plugins/searchgasm-1.0.2/benchmarks/benchmark.rb +++ /dev/null @@ -1,43 +0,0 @@ -require File.dirname(__FILE__) + '/benchmark_helper.rb' - -times = 1 - -Benchmark.bm(20) do |x| - x.report("1st instantiation:") { Account.new_search } - x.report("2nd instantiation:") { Account.new_search } - - # Now that we see the benefits of caching, lets cache the rest of the classes and perform the rest of the tests, - # so that they are fair - User.new_search - Order.new_search - - x.report("Local ordering:") do - times.times do - Account.new_search(:order_by => :name).sanitize - end - end - - x.report("Advanced ordering:") do - times.times do - Account.new_search(:order_by => {:users => {:orders => :total}}).sanitize - end - end - - x.report("Local conditions:") do - times.times do - Account.new_search(:conditions => {:name_like => "Binary"}).sanitize - end - end - - x.report("Advanced conditions:") do - times.times do - Account.new_search(:conditions => {:users => {:orders => {:total_gt => 1}}}).sanitize - end - end - - x.report("Its complicated:") do - times.times do - Account.new_search(:conditions => {:users => {:orders => {:total_gt => 1, :created_at_after => Time.now}, :first_name_like => "Ben"}, :name_begins_with => "Awesome"}, :per_page => 20, :page => 2, :order_by => {:users => {:orders => :total}}, :order_as => "ASC").sanitize - end - end -end \ No newline at end of file diff --git a/vendor/plugins/searchgasm-1.0.2/benchmarks/benchmark_helper.rb b/vendor/plugins/searchgasm-1.0.2/benchmarks/benchmark_helper.rb deleted file mode 100644 index ff5946f..0000000 --- a/vendor/plugins/searchgasm-1.0.2/benchmarks/benchmark_helper.rb +++ /dev/null @@ -1,52 +0,0 @@ -require "rubygems" -require "benchmark" -require "ruby-prof" -require "activerecord" -require File.dirname(__FILE__) + '/../test/libs/acts_as_tree' -require File.dirname(__FILE__) + '/../lib/searchgasm' - -ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:") - -ActiveRecord::Schema.define(:version => 1) do - create_table :accounts do |t| - t.datetime :created_at - t.datetime :updated_at - t.string :name - t.boolean :active - end - - create_table :users do |t| - t.datetime :created_at - t.datetime :updated_at - t.integer :account_id - t.integer :parent_id - t.string :first_name - t.string :last_name - t.boolean :active - t.text :bio - end - - create_table :orders do |t| - t.datetime :created_at - t.datetime :updated_at - t.integer :user_id - t.float :total - t.text :description - t.binary :receipt - end -end - -class Account < ActiveRecord::Base - has_many :users, :dependent => :destroy - has_many :orders, :through => :users -end - -class User < ActiveRecord::Base - acts_as_tree - belongs_to :account - has_many :orders, :dependent => :destroy -end - -class Order < ActiveRecord::Base - belongs_to :user -end \ No newline at end of file diff --git a/vendor/plugins/searchgasm-1.0.2/benchmarks/profile.rb b/vendor/plugins/searchgasm-1.0.2/benchmarks/profile.rb deleted file mode 100644 index 3d2905e..0000000 --- a/vendor/plugins/searchgasm-1.0.2/benchmarks/profile.rb +++ /dev/null @@ -1,15 +0,0 @@ -require File.dirname(__FILE__) + '/benchmark_helper.rb' -require "ruby-prof" - -Account.new_search -User.new_search -Order.new_search - -RubyProf.start - -# Put profile code here - -result = RubyProf.stop - -printer = RubyProf::FlatPrinter.new(result) -printer.print(STDOUT, 0) \ No newline at end of file diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/active_record/associations.rb b/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/active_record/associations.rb deleted file mode 100644 index 1bbecfc..0000000 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/active_record/associations.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Searchgasm - module ActiveRecord - # = Searchgasm ActiveRecord Associations - # - # These methods hook into ActiveRecords association methods and add in searchgasm functionality. - module Associations - module AssociationCollection - # This is an alias method chain. It hook into ActiveRecord's "find" method for associations and checks to see if Searchgasm should get involved. - def find_with_searchgasm(*args) - options = args.extract_options! - args << sanitize_options_with_searchgasm(options) - find_without_searchgasm(*args) - end - - # See build_conditions under Searchgasm::ActiveRecord::Base. This is the same thing but for associations. - def build_conditions(options = {}, &block) - conditions = @reflection.klass.build_conditions(options, &block) - conditions.scope = scope(:find)[:conditions] - conditions - end - - # See build_conditions! under Searchgasm::ActiveRecord::Base. This is the same thing but for associations. - def build_conditions!(options = {}, &block) - conditions = @reflection.klass.build_conditions!(options, &block) - conditions.scope = scope(:find)[:conditions] - conditions - end - - # See build_search under Searchgasm::ActiveRecord::Base. This is the same thing but for associations. - def build_search(options = {}, &block) - conditions = @reflection.klass.build_search(options, &block) - conditions.scope = scope(:find)[:conditions] - conditions - end - - # See build_conditions! under Searchgasm::ActiveRecord::Base. This is the same thing but for associations. - def build_search!(options = {}, &block) - conditions = @reflection.klass.build_search!(options, &block) - conditions.scope = scope(:find)[:conditions] - conditions - end - end - - module HasManyAssociation - # This is an alias method chain. It hook into ActiveRecord's "calculate" method for has many associations and checks to see if Searchgasm should get involved. - def count_with_searchgasm(*args) - column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args) - count_without_searchgasm(column_name, sanitize_options_with_searchgasm(options)) - end - end - end - end -end - -ActiveRecord::Associations::AssociationCollection.send(:include, Searchgasm::ActiveRecord::Associations::AssociationCollection) - -module ActiveRecord - module Associations - class AssociationCollection - alias_method_chain :find, :searchgasm - end - end -end - -ActiveRecord::Associations::HasManyAssociation.send(:include, Searchgasm::ActiveRecord::Associations::HasManyAssociation) - -module ActiveRecord - module Associations - class HasManyAssociation - alias_method_chain :count, :searchgasm - end - end -end \ No newline at end of file diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/utilities.rb b/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/utilities.rb deleted file mode 100644 index 7cc5680..0000000 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/utilities.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Searchgasm - module Utilities # :nodoc: - private - def merge_conditions(*conditions) - options = conditions.extract_options! - conditions.delete_if { |condition| condition.blank? } - return if conditions.blank? - return conditions.first if conditions.size == 1 - - conditions_strs = [] - conditions_subs = [] - - conditions.each do |condition| - next if condition.blank? - arr_condition = [condition].flatten - conditions_strs << arr_condition.first - conditions_subs += arr_condition[1..-1] - end - - return if conditions_strs.blank? - - join = options[:any] ? "OR" : "AND" - conditions_str = "(#{conditions_strs.join(") #{join} (")})" - - return conditions_str if conditions_subs.blank? - - [conditions_str, *conditions_subs] - end - end -end \ No newline at end of file diff --git a/vendor/plugins/searchgasm-1.0.2/CHANGELOG.rdoc b/vendor/plugins/searchgasm-1.1.0/CHANGELOG.rdoc similarity index 55% rename from vendor/plugins/searchgasm-1.0.2/CHANGELOG.rdoc rename to vendor/plugins/searchgasm-1.1.0/CHANGELOG.rdoc index 7a72685..4e834d0 100644 --- a/vendor/plugins/searchgasm-1.0.2/CHANGELOG.rdoc +++ b/vendor/plugins/searchgasm-1.1.0/CHANGELOG.rdoc @@ -1,7 +1,28 @@ +== 1.1.0 released 2008-09-18 + +* Added the options :inner_spread and :outer_spread to the page_links helper. Also added various config options for setting page_links defaults. +* Updated calculation methods to ignore :limit and :offset. AR returns 0 or nil on calculations that provide an offset. +* Added support to allow for "any" of the conditions, instead of all of them. Joins conditions with "or" instead of "and". See Searchgasm::Conditions::Base or the readme + +== 1.0.4 released 2008-09-18 + +* Fixed bugs when performing calculations and searches on has_many through relationships. +* Instead of merging the find_options myself, I delegated that to AR's with_scope function, which already does this. Much more solid, less intrusive. + +== 1.0.3 released 2008-09-18 + +* Updated inspect to show the current options for your search. Plays nicer in the console. +* Made sure protection state is persistent among relationship conditions. +* Fixed bug with backwards compatibility of rails. concat requires a proc in older version. +* Defaulted remote control types to use GET requests instead of POST. +* Completely reengineered integration with ActiveRecord. Searchgasm is properly using scopes letting you do use serachgasm where scope are implemented. @current_users.orders.new_search, etc. If your search is scoped and you want a search object, that search object will represent a new search in the context of those scopes, meaning the scopes get merged into Searchgasm as options. +* Dropped support for Searchgasm functionality when defining relationships: has_many :order, :conditions => {:total_gt => 100}, will not work anymore. It's a chicken and the egg thing. Searchgasm needs AR constants, some models get loaded before others, therefore the Order model may not have been loaded yet, causing an unknown constant error. +* Clean up redundant code and moved it into the Searchgasm::Shared namespace. + == 1.0.2 released 2008-09-12 * Moved cached searchers out of the global namespace and into the Searchgasm::Cache namespce. -* Various changes to improve performance. Added in benchmark reports in readme as well as a benchmarks directory. +* Various changes to improve performance through profiling / benchmarking. http://pastie.org/271936 * Config.per_page works with new_search & new_search! only. Where as before it was only working if the search was protected. == 1.0.1 released 2008-09-11 @@ -50,7 +71,7 @@ == 0.9.4 released 2008-09-03 * Cleaned up search methods -* Removed reset!method for both searching and searching by conditions +* Removed reset! method for both searching and searching by conditions == 0.9.3 released 2008-09-02 diff --git a/vendor/plugins/searchgasm-1.0.2/MIT-LICENSE b/vendor/plugins/searchgasm-1.1.0/MIT-LICENSE similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/MIT-LICENSE rename to vendor/plugins/searchgasm-1.1.0/MIT-LICENSE diff --git a/vendor/plugins/searchgasm-1.0.2/Manifest b/vendor/plugins/searchgasm-1.1.0/Manifest similarity index 94% rename from vendor/plugins/searchgasm-1.0.2/Manifest rename to vendor/plugins/searchgasm-1.1.0/Manifest index fa0beca..4619b56 100644 --- a/vendor/plugins/searchgasm-1.0.2/Manifest +++ b/vendor/plugins/searchgasm-1.1.0/Manifest @@ -1,6 +1,3 @@ -benchmarks/benchmark.rb -benchmarks/benchmark_helper.rb -benchmarks/profile.rb CHANGELOG.rdoc examples/README.rdoc init.rb @@ -42,7 +39,10 @@ lib/searchgasm/search/conditions.rb lib/searchgasm/search/ordering.rb lib/searchgasm/search/pagination.rb lib/searchgasm/search/protection.rb -lib/searchgasm/utilities.rb +lib/searchgasm/search.rb +lib/searchgasm/shared/searching.rb +lib/searchgasm/shared/utilities.rb +lib/searchgasm/shared/virtual_classes.rb lib/searchgasm/version.rb lib/searchgasm.rb Manifest diff --git a/vendor/plugins/searchgasm-1.0.2/README.rdoc b/vendor/plugins/searchgasm-1.1.0/README.rdoc similarity index 87% rename from vendor/plugins/searchgasm-1.0.2/README.rdoc rename to vendor/plugins/searchgasm-1.1.0/README.rdoc index 939bc61..27a23f6 100644 --- a/vendor/plugins/searchgasm-1.0.2/README.rdoc +++ b/vendor/plugins/searchgasm-1.1.0/README.rdoc @@ -147,7 +147,7 @@ Take the @search object and pass it right into form\_for or fields\_for (see abo Using the object from above: @search.average('id') - @search.count + @search.count # ignores limit and offset @search.maximum('id') @search.minimum('id') @search.sum('id') @@ -189,6 +189,16 @@ Don't need pagination, ordering, or any of the other options? Search with condit Pass a conditions object right into ActiveRecord: User.all(:conditions => conditions) + +== Match ANY or ALL of the conditions + +As you saw above, the nice thing about Searchgasm is it's integration with forms. I designed the "any" option so that forms can set this as well, just like a condition. + + search = User.new_search(:conditions => {:age_gt => 18}) + search.conditions.first_name_contains = "Ben" + search.conditions.any = true # can set this to "true" or "1" or "yes" + search.all # will join all conditions with "or" instead of "and" + # ... all operations above are available == Scoped searching @@ -219,14 +229,23 @@ For tree data structures you get a few nifty methods. Let's assume Users is a tr User.all(:conditions => {:inclusive_descendant_of => User.roots.first.id}) -== Available anywhere (relationships & scopes) +== Scope support -Not only can you use searchgasm when searching, but you can use it when setting up relationships or scopes. Anywhere you specify conditions in ActiveRecord. +Not only can you use searchgasm when searching, but you can use it when using scopes. class User < ActiveRecord::Base - has_many :expensive_pending_orders, :conditions => {:total_greater_than => 1_000_000, :state => :pending}, :per_page => 20 named_scope :sexy, :conditions => {:first_name => "Ben", email_ends_with => "binarylogic.com"}, :per_page => 20 end + +or + + class User < ActiveRecord::Base + def self.find_sexy + with_scope(:find => {:conditions => {:first_name => "Ben", email_ends_with => "binarylogic.com"}, :per_page => 20}) do + all + end + end + end == Always use protection...against SQL injections @@ -328,22 +347,11 @@ Pretty nifty, huh? You can create any condition ultimately creating any SQL you == Under the hood -I'm a big fan of understanding what I'm using, so here's a quick explanation: The design behind this plugin is pretty simple. The search object "sanitizes" down into the options passed into ActiveRecord::Base.find(). It serves as a transparent filter between you and ActiveRecord::Base.find(). This filter provides "enhancements" that get translated into options that ActiveRecord::Base.find() can understand. It doesn't dig into the ActiveRecord internals, it only uses what is publicly available. It jumps in and helps out only when needed, otherwise it sits back and lets ActiveRecord do all of the work. Between that and the extensive tests, this is a solid and fast plugin. - -== Performance / Benchmarking - -I ran searchgasm through some performance tests using ruby-prof. After working on it for a little while I improved performance quite a bit. Notice the "2nd instantiation" report. This is implementing caching and skips all dynamic method creation / meta programming. It resulted in code over 50 times faster. +I'm a big fan of understanding what I'm using, so here's a quick explanation: The design behind this plugin is pretty simple and I had 1 main rule when developing this: - user system total real - 1st instantiation: 0.000000 0.000000 0.000000 ( 0.005466) - 2nd instantiation: 0.000000 0.000000 0.000000 ( 0.000108) - Local ordering: 0.000000 0.000000 0.000000 ( 0.000265) - Advanced ordering: 0.000000 0.000000 0.000000 ( 0.000413) - Local conditions: 0.000000 0.000000 0.000000 ( 0.000241) - Advanced conditions: 0.000000 0.000000 0.000000 ( 0.000602) - Its complicated: 0.000000 0.000000 0.000000 ( 0.001017) +ActiveRecord should never know about Searchgasm -I also included the benchmarking file in benchmarks/benchmark.rb to see for yourself. +What that rule means is that any options you pass when searching get "sanitized" down into options ActiveRecord can understand. Searchgasm serves as a transparent filter between you and ActiveRecord. It doesn't dig into the ActiveRecord internals, it only uses what is publicly available. It jumps in and helps out only when needed, otherwise it sits back and stays completely out of the way. Between that and the extensive tests, this is a solid and fast plugin. == Reporting problems / bugs diff --git a/vendor/plugins/searchgasm-1.0.2/Rakefile b/vendor/plugins/searchgasm-1.1.0/Rakefile similarity index 67% rename from vendor/plugins/searchgasm-1.0.2/Rakefile rename to vendor/plugins/searchgasm-1.1.0/Rakefile index 8d1c6b5..40602da 100644 --- a/vendor/plugins/searchgasm-1.0.2/Rakefile +++ b/vendor/plugins/searchgasm-1.1.0/Rakefile @@ -8,8 +8,7 @@ Echoe.new 'searchgasm' do |p| p.author = "Ben Johnson of Binary Logic" p.email = 'bjohnson@binarylogic.com' p.project = 'searchgasm' - p.summary = "Orgasmic ActiveRecord searching" - p.description = "Makes ActiveRecord searching easier, robust, and powerful. Automatic conditions, pagination support, object based searching, and more." + p.summary = "Object based ActiveRecord searching, ordering, pagination, and more!" p.url = "http://github.com/binarylogic/searchgasm" p.dependencies = %w(activerecord activesupport) p.include_rakefile = true diff --git a/vendor/plugins/searchgasm-1.0.2/examples/README.rdoc b/vendor/plugins/searchgasm-1.1.0/examples/README.rdoc similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/examples/README.rdoc rename to vendor/plugins/searchgasm-1.1.0/examples/README.rdoc diff --git a/vendor/plugins/searchgasm-1.0.2/init.rb b/vendor/plugins/searchgasm-1.1.0/init.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/init.rb rename to vendor/plugins/searchgasm-1.1.0/init.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm.rb similarity index 94% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm.rb index caa4811..2cb8b84 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm.rb @@ -6,10 +6,14 @@ # Core Ext require "searchgasm/core_ext/hash" -# Utilties +# Shared +require "searchgasm/shared/utilities" +require "searchgasm/shared/searching" +require "searchgasm/shared/virtual_classes" + +# Base classes require "searchgasm/version" require "searchgasm/config" -require "searchgasm/utilities" # ActiveRecord require "searchgasm/active_record/base" diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/active_record.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/active_record.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/active_record.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/active_record.rb diff --git a/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/active_record/associations.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/active_record/associations.rb new file mode 100644 index 0000000..c720247 --- /dev/null +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/active_record/associations.rb @@ -0,0 +1,61 @@ +module Searchgasm + module ActiveRecord + # = Searchgasm ActiveRecord Associations + # + # These methods hook into ActiveRecords association methods and add in searchgasm functionality. + module Associations + module AssociationCollection + # This needs to be implemented because AR doesn't leverage scopes with this method like it probably should + def find_with_searchgasm(*args) + options = args.extract_options! + args << sanitize_options_with_searchgasm(options) + find_without_searchgasm(*args) + end + + # See build_conditions under Searchgasm::ActiveRecord::Base. This is the same thing but for associations. + def build_conditions(options = {}, &block) + @reflection.klass.send(:with_scope, :find => construct_scope[:find]) { @reflection.klass.build_conditions(options, &block) } + end + + # See build_conditions! under Searchgasm::ActiveRecord::Base. This is the same thing but for associations. + def build_conditions!(options = {}, &block) + @reflection.klass.send(:with_scope, :find => construct_scope[:find]) { @reflection.klass.build_conditions!(options, &block) } + end + + # See build_search under Searchgasm::ActiveRecord::Base. This is the same thing but for associations. + def build_search(options = {}, &block) + @reflection.klass.send(:with_scope, :find => construct_scope[:find]) { @reflection.klass.build_search(options, &block) } + end + + # See build_conditions! under Searchgasm::ActiveRecord::Base. This is the same thing but for associations. + def build_search!(options = {}, &block) + @reflection.klass.send(:with_scope, :find => construct_scope[:find]) { @reflection.klass.build_search!(options, &block) } + end + end + + module Shared + def count_with_searchgasm(*args) + options = args.extract_options! + args << sanitize_options_with_searchgasm(options) + count_without_searchgasm(*args) + end + end + end + end +end + +module ActiveRecord + module Associations + class AssociationCollection + include Searchgasm::ActiveRecord::Associations::AssociationCollection + + alias_method_chain :find, :searchgasm + end + + class HasManyAssociation + include Searchgasm::ActiveRecord::Associations::Shared + + alias_method_chain :count, :searchgasm + end + end +end \ No newline at end of file diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/active_record/base.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/active_record/base.rb similarity index 76% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/active_record/base.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/active_record/base.rb index 2b6cc7f..104d031 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/active_record/base.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/active_record/base.rb @@ -11,6 +11,12 @@ def calculate_with_searchgasm(*args) calculate_without_searchgasm(*args) end + def total(*args) + options = args.extract_options! + searcher = searchgasm_searcher(options) + searcher.total + end + # This is an alias method chain. It hooks into ActiveRecord's "find" method and checks to see if Searchgasm should get involved. def find_with_searchgasm(*args) options = args.extract_options! @@ -18,21 +24,29 @@ def find_with_searchgasm(*args) args << options find_without_searchgasm(*args) end - + # This is an alias method chain. It hooks into ActiveRecord's scopes and checks to see if Searchgasm should get involved. Allowing you to use all of Searchgasms conditions and tools # in scopes as well. # # === Examples # + # Named scopes: + # # named_scope :top_expensive, :conditions => {:total_gt => 1_000_000}, :per_page => 10 + # named_scope :top_expensive_ordered, :conditions => {:total_gt => 1_000_000}, :per_page => 10, :order_by => {:user => :first_name} + # + # Good ole' regular scopes: # # with_scope(:find => {:conditions => {:total_gt => 1_000_000}, :per_page => 10}) do # find(:all) # end - def scope_with_searchgasm(method, key = nil) - scope = scope_without_searchgasm(method, key) - return sanitize_options_with_searchgasm(scope) if key.nil? && method == :find && !scope.blank? - scope + # + # with_scope(:find => {:conditions => {:total_gt => 1_000_000}, :per_page => 10}) do + # build_search + # end + def with_scope_with_searchgasm(method_scoping = {}, action = :merge, &block) + method_scoping[:find] = sanitize_options_with_searchgasm(method_scoping[:find]) if method_scoping[:find] + with_scope_without_searchgasm(method_scoping, action, &block) end # This is a special method that Searchgasm adds in. It returns a new conditions object on the model. So you can search by conditions *only*. @@ -118,17 +132,36 @@ def accessible_conditions # :nodoc: private def sanitize_options_with_searchgasm(options = {}) return options unless Searchgasm::Search::Base.needed?(self, options) - search = searchgasm_searcher(options) + search = Searchgasm::Search::Base.create_virtual_class(self).new # call explicitly to avoid merging the scopes into the searcher search.acting_as_filter = true + search.options = options search.sanitize end def searchgasm_conditions(options = {}) - Searchgasm::Conditions::Base.create_virtual_class(self).new(options) + searcher = nil + conditions = nil + conditions = options.delete(:conditions) if options[:conditions].is_a?(Hash) + + with_scope(:find => {:conditions => options[:conditions]}) do + searcher = Searchgasm::Conditions::Base.create_virtual_class(self).new(scope(:find)[:conditions]) + searcher.conditions = conditions unless conditions.nil? + end + + searcher end def searchgasm_searcher(options = {}) - Searchgasm::Search::Base.create_virtual_class(self).new(options) + searcher = nil + conditions = nil + conditions = options.delete(:conditions) if options[:conditions].is_a?(Hash) + + with_scope(:find => options) do + searcher = Searchgasm::Search::Base.create_virtual_class(self).new(scope(:find)) + searcher.conditions = conditions unless conditions.nil? + end + + searcher end end end @@ -141,7 +174,7 @@ class Base class << self alias_method_chain :calculate, :searchgasm alias_method_chain :find, :searchgasm - alias_method_chain :scope, :searchgasm + alias_method_chain :with_scope, :searchgasm alias_method :new_conditions, :build_conditions alias_method :new_conditions!, :build_conditions! alias_method :new_search, :build_search diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/base.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/base.rb similarity index 99% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/base.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/base.rb index 6b41757..c2660ad 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/base.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/base.rb @@ -5,7 +5,7 @@ module Condition # :nodoc: # The base class for creating a condition. Your custom conditions should extend this class. # See Searchgasm::Conditions::Base.register_condition on how to write your own condition. class Base - include Utilities + include Shared::Utilities attr_accessor :column, :klass attr_reader :value diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/begins_with.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/begins_with.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/begins_with.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/begins_with.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/child_of.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/child_of.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/child_of.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/child_of.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/contains.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/contains.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/contains.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/contains.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/descendant_of.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/descendant_of.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/descendant_of.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/descendant_of.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/does_not_equal.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/does_not_equal.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/does_not_equal.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/does_not_equal.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/ends_with.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/ends_with.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/ends_with.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/ends_with.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/equals.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/equals.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/equals.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/equals.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/greater_than.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/greater_than.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/greater_than.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/greater_than.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/greater_than_or_equal_to.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/greater_than_or_equal_to.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/greater_than_or_equal_to.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/greater_than_or_equal_to.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/inclusive_descendant_of.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/inclusive_descendant_of.rb similarity index 89% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/inclusive_descendant_of.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/inclusive_descendant_of.rb index ea11cc8..cfbe974 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/inclusive_descendant_of.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/inclusive_descendant_of.rb @@ -1,8 +1,6 @@ module Searchgasm module Condition class InclusiveDescendantOf < Tree - include Searchgasm::Utilities - def to_conditions(value) condition = DescendantOf.new(klass, column) condition.value = value diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/keywords.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/keywords.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/keywords.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/keywords.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/less_than.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/less_than.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/less_than.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/less_than.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/less_than_or_equal_to.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/less_than_or_equal_to.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/less_than_or_equal_to.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/less_than_or_equal_to.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/sibling_of.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/sibling_of.rb similarity index 93% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/sibling_of.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/sibling_of.rb index 4397f96..2e90c65 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/sibling_of.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/sibling_of.rb @@ -1,8 +1,6 @@ module Searchgasm module Condition class SiblingOf < Tree - include Searchgasm::Utilities - def to_conditions(value) parent_association = klass.reflect_on_association(:parent) foreign_key_name = (parent_association && parent_association.options[:foreign_key]) || "parent_id" diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/tree.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/tree.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/condition/tree.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/condition/tree.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/conditions/base.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/conditions/base.rb similarity index 81% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/conditions/base.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/conditions/base.rb index efaefdf..2455bd5 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/conditions/base.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/conditions/base.rb @@ -5,9 +5,11 @@ module Conditions # :nodoc: # Represents a collection of conditions and performs various tasks on that collection. For information on each condition see Searchgasm::Condition. # Each condition has its own file and class and the source for each condition is pretty self explanatory. class Base - include Utilities + include Searchgasm::Shared::Utilities + include Searchgasm::Shared::Searching + include Searchgasm::Shared::VirtualClasses - attr_accessor :relationship_name, :scope + attr_accessor :any, :relationship_name, :sql class << self attr_accessor :added_klass_conditions, :added_column_conditions, :added_associations @@ -64,37 +66,15 @@ def condition_names def needed?(model_class, conditions) # :nodoc: if conditions.is_a?(Hash) + return true if conditions[:any] + column_names = model_class.column_names conditions.stringify_keys.keys.each do |condition| - return true unless model_class.column_names.include?(condition) + return true unless column_names.include?(condition) end end false end - - # Creates virtual classes for the class passed to it. This is a neccesity for keeping dynamically created method - # names specific to models. It provides caching and helps a lot with performance. - def create_virtual_class(model_class) - class_search_name = "::Searchgasm::Cache::#{model_class.name}Conditions" - - begin - eval(class_search_name) - rescue NameError - eval <<-end_eval - class #{class_search_name} < ::Searchgasm::Conditions::Base - def self.klass - #{model_class.name} - end - - def klass - #{model_class.name} - end - end - - #{class_search_name} - end_eval - end - end end def initialize(init_conditions = {}) @@ -104,43 +84,60 @@ def initialize(init_conditions = {}) self.conditions = init_conditions end - # Setup methods for searching - [:all, :average, :calculate, :count, :find, :first, :maximum, :minimum, :sum].each do |method| - class_eval <<-"end_eval", __FILE__, __LINE__ - def #{method}(*args) - self.conditions = args.extract_options! - args << {:conditions => sanitize} - klass.#{method}(*args) - end - end_eval + # Determines if we should join the conditions with "AND" or "OR". + # + # === Examples + # + # search.conditions.any = true # will join all conditions with "or", you can also set this to "true", "1", or "yes" + # search.conditions.any = false # will join all conditions with "and" + def any=(value) + associations.each { |association| association.any = value } + @any = value + end + + def any # :nodoc: + any? + end + + # Convenience method for determining if we should join the conditions with "AND" or "OR". + def any? + @any == true || @any == "true" || @any == "1" || @any == "yes" end # A list of includes to use when searching, includes relationships def includes i = [] associations.each do |association| + next if association.conditions.blank? association_includes = association.includes i << (association_includes.blank? ? association.relationship_name.to_sym : {association.relationship_name.to_sym => association_includes}) end i.blank? ? nil : (i.size == 1 ? i.first : i) end + def inspect + conditions_hash = conditions + conditions_hash[:sql] = sql if sql + conditions_hash[:protected] = true if protected? + conditions_hash.inspect + end + # Sanitizes the conditions down into conditions that ActiveRecord::Base.find can understand. def sanitize - conditions = merge_conditions(*objects.collect { |object| object.sanitize }) - return scope if conditions.blank? - merge_conditions(conditions, scope) + conditions = merge_conditions(*(objects.collect { |object| object.sanitize } << {:any => any})) + return sql if conditions.blank? + merged_conditions = merge_conditions(conditions, sql, :any => any) + merged_conditions end - # Allows you to set the conditions via a hash. If you do not pass a hash it will set scope instead, so that you can continue to add conditions and ultimately - # merge it all together at the end. + # Allows you to set the conditions via a hash. def conditions=(conditions) case conditions when Hash assert_valid_conditions(conditions) remove_conditions_from_protected_assignement(conditions).each { |condition, value| send("#{condition}=", value) } else - self.scope = conditions + self.sql = conditions end end @@ -148,8 +145,7 @@ def conditions=(conditions) def conditions conditions_hash = {} objects.each do |object| - case object - when self.class + if object.class < Searchgasm::Conditions::Base relationship_conditions = object.conditions next if relationship_conditions.blank? conditions_hash[object.relationship_name.to_sym] = relationship_conditions @@ -171,8 +167,9 @@ def add_associations! self.class.class_eval <<-"end_eval", __FILE__, __LINE__ def #{association.name} if @#{association.name}.nil? - @#{association.name} = #{association.class_name}.new_conditions + @#{association.name} = Searchgasm::Conditions::Base.create_virtual_class(#{association.class_name}).new @#{association.name}.relationship_name = "#{association.name}" + @#{association.name}.protect = protect objects << @#{association.name} end @#{association.name} @@ -242,7 +239,7 @@ def add_klass_conditions! end def assert_valid_conditions(conditions) - conditions.stringify_keys.fast_assert_valid_keys(self.class.condition_names + self.class.association_names) + conditions.stringify_keys.fast_assert_valid_keys(self.class.condition_names + self.class.association_names + ["any"]) end def associations diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/conditions/protection.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/conditions/protection.rb similarity index 66% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/conditions/protection.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/conditions/protection.rb index bc9b1e4..c0a97c4 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/conditions/protection.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/conditions/protection.rb @@ -6,7 +6,7 @@ module Conditions module Protection def self.included(klass) klass.class_eval do - attr_accessor :protect + attr_reader :protect alias_method_chain :conditions=, :protection end end @@ -15,16 +15,22 @@ def conditions_with_protection=(conditions) unless conditions.is_a?(Hash) if protect? return if conditions.blank? - raise(ArgumentError, "You can not set a scope or pass SQL while the search is being protected") + raise(ArgumentError, "You can not pass SQL as conditions while the search is being protected, you can only pass a hash") end end self.conditions_without_protection = conditions end + def protect=(value) + associations.each { |association| association.protect = value } + @protect = value + end + def protect? protect == true end + alias_method :protected?, :protect? end end end \ No newline at end of file diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/config.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/config.rb similarity index 55% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/config.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/config.rb index 00afb75..3cefabb 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/config.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/config.rb @@ -43,6 +43,78 @@ def desc_indicator=(value) @desc_indicator = value end + def page_links_first # :nodoc: + @page_links_first + end + + # The default for the :first option for the page_links helper. + # + # * Default: nil + # * Accepts: Anything you want, text, html, etc. nil to disable + def page_links_first=(value) + @page_links_first = value + end + + def page_links_last # :nodoc: + @page_links_last + end + + # The default for the :last option for the page_links helper. + # + # * Default: nil + # * Accepts: Anything you want, text, html, etc. nil to disable + def page_links_last=(value) + @page_links_last = value + end + + def page_links_inner_spread # :nodoc: + @page_links_inner_spread ||= 3 + end + + # The default for the :inner_spread option for the page_links helper. + # + # * Default: 3 + # * Accepts: Any integer >= 1, set to nil to show all pages + def page_links_inner_spread=(value) + @page_links_inner_spread = value + end + + def page_links_outer_spread # :nodoc: + @page_links_outer_spread ||= 2 + end + + # The default for the :outer_spread option for the page_links helper. + # + # * Default: 2 + # * Accepts: Any integer >= 1, set to nil to display, 0 to only show the "..." separator + def page_links_outer_spread=(value) + @page_links_outer_spread = value + end + + def page_links_next # :nodoc: + @page_links_next ||= "< Next" + end + + # The default for the :next option for the page_links helper. + # + # * Default: "< Next" + # * Accepts: Anything you want, text, html, etc. nil to disable + def page_links_next=(value) + @page_links_next = value + end + + def page_links_prev # :nodoc: + @page_links_prev ||= "< Prev" + end + + # The default for the :prev option for the page_links helper. + # + # * Default: "< Prev" + # * Accepts: Anything you want, text, html, etc. nil to disable + def page_links_prev=(value) + @page_links_prev = value + end + def per_page # :nodoc: @per_page end diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/core_ext/hash.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/core_ext/hash.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/core_ext/hash.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/core_ext/hash.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/link.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/link.rb similarity index 100% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/link.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/link.rb diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/links.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/links.rb similarity index 67% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/links.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/links.rb index 55229f9..acb32f4 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/links.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/links.rb @@ -72,7 +72,7 @@ def per_page_links(options = {}) # page_links # page_links(:first => "<< First", :last => "Last >>") # - # === Classes and tag + # === Classes and tags # # If the user is on the current page they will get a tag, not an tag. If they are on the first page the "first" and "prev" options will be a also. The same goes # for "next" and "last" if the user is on the last page. Other than that each element will come with a CSS class so you can style it to your liking. Somtimes the easiest way to understand this @@ -80,7 +80,7 @@ def per_page_links(options = {}) # # * page - This is in *every* element, span or a. # * first_page - This is for the "first page" element only. - # * preve_page - This is for the "prev page" element only. + # * prev_page - This is for the "prev page" element only. # * current_page - This is for the current page element # * next_page - This is for the "next page" element only. # * last_page - This is for the "last page" element only. @@ -90,7 +90,13 @@ def per_page_links(options = {}) # # Please look at per_page_link. All options there are applicable here and are passed onto each option. # - # * :spread -- default: 3, set to nil to show all page, this represents how many choices available on each side of the current page + # * :inner_spread -- default: 3, set to nil to show all pages, this represents how many choices available on each side of the current page + # * :outer_spread -- default: 1, set to nil to disable, this represents how many choices are in the "outer" spread. If set to 0, the separator will be present with no page links. This option changes the links from + # * "< Prev 2 3 4 [5] 6 7 8 Next >" with 10 total pages to: + # * "< Prev ... 3 4 [5] 6 7 ... Next >" for :outer_spread = 0 and :inner_spread = 3 + # * "< Prev 1 ... 3 4 [5] 6 7 ... 10 Next >" for :outer_spread = 1 and :inner_spread = 3 + # * "< Prev 1 2 ... 3 4 [5] 6 7 ... 9 10 Next >" for :outer_spread = 2 and :inner_spread = 3 (outer_spread = number of absolute pages on each side) + # * Outer spread pages will not be visible unless the current_page is more than :inner_spread away from the first or last page. # * :prev -- default: < Prev, set to nil to omit. This is an extra link on the left side of the page links that will go to the previous page # * :next -- default: Next >, set to nil to omit. This is an extra link on the right side of the page links that will go to the next page # * :first -- default: nil, set to nil to omit. This is an extra link on thefar left side of the page links that will go to the first page @@ -99,22 +105,47 @@ def page_links(options = {}) add_page_links_defaults!(options) return if options[:last_page] <= 1 - first_page = 0 - last_page = 0 - if !options[:spread].blank? - first_page = options[:current_page] - options[:spread] - first_page = options[:first_page] if first_page < options[:first_page] - last_page = options[:current_page] + options[:spread] - last_page = options[:last_page] if last_page > options[:last_page] + inner_spread_start = inner_spread_end = lower_gap = lower_outer_spread_start = lower_outer_spread_end = upper_gap = upper_outer_spread_start = upper_outer_spread_end = 0 + if !options[:inner_spread].blank? + inner_spread_start = options[:current_page] - options[:inner_spread] + inner_spread_start = options[:first_page] if inner_spread_start < options[:first_page] + inner_spread_end = options[:current_page] + options[:inner_spread] + inner_spread_end = options[:last_page] if inner_spread_end > options[:last_page] + + if !options[:outer_spread].blank? + lower_gap = inner_spread_start - options[:first_page] + if lower_gap > 0 + lower_outer_spread_start = options[:first_page] + lower_outer_spread_end = options[:outer_spread] > lower_gap ? lower_gap : options[:outer_spread] + end + + upper_gap = options[:last_page] - inner_spread_end + if upper_gap > 0 + upper_outer_spread_start = options[:last_page] - (options[:outer_spread] > upper_gap ? upper_gap : options[:outer_spread]) + 1 + upper_outer_spread_end = options[:last_page] + end + end else - first_page = options[:first_page] - last_page = options[:last_page] + inner_spread_start = options[:first_page] + inner_spread_end = options[:last_page] end html = "" html += span_or_page_link(:first, options.deep_dup, options[:current_page] == options[:first_page]) if options[:first] html += span_or_page_link(:prev, options.deep_dup, options[:current_page] == options[:first_page]) if options[:prev] - (first_page..last_page).each { |page| html += span_or_page_link(page, options.deep_dup, page == options[:current_page]) } + + if lower_gap > 0 + (lower_outer_spread_start..lower_outer_spread_end).each { |page| html += span_or_page_link(page, options.deep_dup, false) } + html += content_tag(:span, "…", options[:html]) if (inner_spread_start - lower_outer_spread_end) > 1 + end + + (inner_spread_start..inner_spread_end).each { |page| html += span_or_page_link(page, options.deep_dup, page == options[:current_page]) } + + if upper_gap > 0 + html += content_tag(:span, "…", options[:html]) if (upper_outer_spread_start - inner_spread_end) > 1 + (upper_outer_spread_start..upper_outer_spread_end).each { |page| html += span_or_page_link(page, options.deep_dup, false) } + end + html += span_or_page_link(:next, options.deep_dup, options[:current_page] == options[:last_page]) if options[:next] html += span_or_page_link(:last, options.deep_dup, options[:current_page] == options[:last_page]) if options[:last] html @@ -151,9 +182,12 @@ def add_page_links_defaults!(options) options[:first_page] ||= 1 options[:last_page] ||= options[:search_obj].page_count options[:current_page] ||= options[:search_obj].page - options[:spread] = 3 unless options.has_key?(:spread) - options[:prev] = "< Prev" unless options.has_key?(:prev) - options[:next] = "Next >" unless options.has_key?(:next) + options[:inner_spread] = Config.page_links_inner_spread unless options.has_key?(:inner_spread) + options[:outer_spread] = Config.page_links_outer_spread unless options.has_key?(:outer_spread) + options[:prev] = Config.page_links_prev unless options.has_key?(:prev) + options[:next] = Config.page_links_next unless options.has_key?(:next) + options[:first] = Config.page_links_first unless options.has_key?(:first) + options[:last] = Config.page_links_last unless options.has_key?(:last) options end diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/remote_link.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/remote_link.rb similarity index 97% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/remote_link.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/remote_link.rb index a97817c..5cfafcb 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/remote_link.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/remote_link.rb @@ -76,6 +76,7 @@ def remote_page_link(page, options = {}) private def add_remote_defaults!(options) options[:remote] ||= {} + options[:remote][:method] ||= :get options[:is_remote] = true end end diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/remote_links.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/remote_links.rb similarity index 90% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/remote_links.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/remote_links.rb index 6340ddb..67ace29 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/remote_links.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/remote_links.rb @@ -1,6 +1,9 @@ module Searchgasm module Helpers module ControlTypes + # = Remote Links Control Types + # + # These helpers use rails built in remote_function as links. They are the same thing as the Links control type, but just use rails built in remote helpers. module RemoteLinks # Same as order_by_links, but uses link_to_remote instead of remote. # diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/remote_select.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/remote_select.rb similarity index 87% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/remote_select.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/remote_select.rb index 4e5358a..416459e 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/remote_select.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/remote_select.rb @@ -1,6 +1,9 @@ module Searchgasm module Helpers module ControlTypes + # = Remote Select Control Types + # + # These helpers use rails built in remote_function as links. They are the same thing as the Select control type, but just use rails built in remote helpers. module RemoteSelect # Please see order_by_links. All options are the same and applicable here. The only difference is that instead of a group of links, this gets returned as a select form element that will perform the same function when the value is changed. def remote_order_by_select(options = {}) diff --git a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/select.rb b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/select.rb similarity index 95% rename from vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/select.rb rename to vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/select.rb index 1d8976e..27b5945 100644 --- a/vendor/plugins/searchgasm-1.0.2/lib/searchgasm/helpers/control_types/select.rb +++ b/vendor/plugins/searchgasm-1.1.0/lib/searchgasm/helpers/control_types/select.rb @@ -1,6 +1,9 @@ module Searchgasm module Helpers module ControlTypes + # = Select Control Types + # + # These create def image_submit_tag(source, options = {}) + options.stringify_keys! + + if confirm = options.delete("confirm") + options["onclick"] ||= '' + options["onclick"] += "return #{confirm_javascript_function(confirm)};" + end + tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options.stringify_keys) end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/scriptaculous_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/scriptaculous_helper.rb index b938c1a..1d01daf 100644 --- a/vendor/rails/actionpack/lib/action_view/helpers/scriptaculous_helper.rb +++ b/vendor/rails/actionpack/lib/action_view/helpers/scriptaculous_helper.rb @@ -95,7 +95,7 @@ def visual_effect(name, element_id = false, js_options = {}) # * :containment - Takes an element or array of elements to treat as # potential drop targets (defaults to the original target element). # - # * :only - A CSS class name or arry of class names used to filter + # * :only - A CSS class name or array of class names used to filter # out child elements as candidates. # # * :scroll - Determines whether to scroll the list during drag diff --git a/vendor/rails/actionpack/lib/action_view/renderable.rb b/vendor/rails/actionpack/lib/action_view/renderable.rb index d960335..0134bc9 100644 --- a/vendor/rails/actionpack/lib/action_view/renderable.rb +++ b/vendor/rails/actionpack/lib/action_view/renderable.rb @@ -1,8 +1,7 @@ module ActionView - module Renderable - # NOTE: The template that this mixin is beening include into is frozen - # So you can not set or modify any instance variables - + # NOTE: The template that this mixin is being included into is frozen + # so you cannot set or modify any instance variables + module Renderable #:nodoc: extend ActiveSupport::Memoizable def self.included(base) @@ -33,10 +32,11 @@ def render(view, local_assigns = {}) view.send(:_set_controller_content_type, mime_type) if respond_to?(:mime_type) view.send(method_name(local_assigns), local_assigns) do |*names| - if proc = view.instance_variable_get("@_proc_for_layout") + ivar = :@_proc_for_layout + if view.instance_variable_defined?(ivar) and proc = view.instance_variable_get(ivar) view.capture(*names, &proc) - else - view.instance_variable_get("@content_for_#{names.first || 'layout'}") + elsif view.instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}") + view.instance_variable_get(ivar) end end end @@ -72,12 +72,9 @@ def #{render_symbol}(local_assigns) end_src begin - logger = defined?(ActionController) && Base.logger - logger.debug "Compiling template #{render_symbol}" if logger - ActionView::Base::CompiledTemplates.module_eval(source, filename, 0) rescue Exception => e # errors from template code - if logger + if logger = defined?(ActionController) && Base.logger logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}" logger.debug "Function body: #{source}" logger.debug "Backtrace: #{e.backtrace.join("\n")}" diff --git a/vendor/rails/actionpack/lib/action_view/renderable_partial.rb b/vendor/rails/actionpack/lib/action_view/renderable_partial.rb index 123a9ae..d92ff1b 100644 --- a/vendor/rails/actionpack/lib/action_view/renderable_partial.rb +++ b/vendor/rails/actionpack/lib/action_view/renderable_partial.rb @@ -1,8 +1,7 @@ module ActionView - module RenderablePartial - # NOTE: The template that this mixin is beening include into is frozen - # So you can not set or modify any instance variables - + # NOTE: The template that this mixin is being included into is frozen + # so you cannot set or modify any instance variables + module RenderablePartial #:nodoc: extend ActiveSupport::Memoizable def variable_name @@ -30,10 +29,13 @@ def render_partial(view, object = nil, local_assigns = {}, as = nil) local_assigns[variable_name] if view.respond_to?(:controller) - object ||= ActiveSupport::Deprecation::DeprecatedObjectProxy.new( - view.controller.instance_variable_get("@#{variable_name}"), - "@#{variable_name} will no longer be implicitly assigned to #{variable_name}" - ) + ivar = :"@#{variable_name}" + object ||= + if view.controller.instance_variable_defined?(ivar) + ActiveSupport::Deprecation::DeprecatedObjectProxy.new( + view.controller.instance_variable_get(ivar), + "#{ivar} will no longer be implicitly assigned to #{variable_name}") + end end # Ensure correct object is reassigned to other accessors diff --git a/vendor/rails/actionpack/test/controller/routing_test.rb b/vendor/rails/actionpack/test/controller/routing_test.rb index 8bb1c49..1eb26a7 100644 --- a/vendor/rails/actionpack/test/controller/routing_test.rb +++ b/vendor/rails/actionpack/test/controller/routing_test.rb @@ -1375,6 +1375,36 @@ def test_failed_requirements_raises_exception_with_violated_requirements x.send(:foo_with_requirement_url, "I am Against the requirements") end end + + def test_routes_changed_correctly_after_clear + ActionController::Base.optimise_named_routes = true + rs = ::ActionController::Routing::RouteSet.new + rs.draw do |r| + r.connect 'ca', :controller => 'ca', :action => "aa" + r.connect 'cb', :controller => 'cb', :action => "ab" + r.connect 'cc', :controller => 'cc', :action => "ac" + r.connect ':controller/:action/:id' + r.connect ':controller/:action/:id.:format' + end + + hash = rs.recognize_path "/cc" + + assert_not_nil hash + assert_equal %w(cc ac), [hash[:controller], hash[:action]] + + rs.draw do |r| + r.connect 'cb', :controller => 'cb', :action => "ab" + r.connect 'cc', :controller => 'cc', :action => "ac" + r.connect ':controller/:action/:id' + r.connect ':controller/:action/:id.:format' + end + + hash = rs.recognize_path "/cc" + + assert_not_nil hash + assert_equal %w(cc ac), [hash[:controller], hash[:action]] + + end end class RouteTest < Test::Unit::TestCase diff --git a/vendor/rails/actionpack/test/controller/session/cookie_store_test.rb b/vendor/rails/actionpack/test/controller/session/cookie_store_test.rb index 5adaeaf..010c00f 100644 --- a/vendor/rails/actionpack/test/controller/session/cookie_store_test.rb +++ b/vendor/rails/actionpack/test/controller/session/cookie_store_test.rb @@ -36,7 +36,9 @@ def self.default_session_options 'session_key' => '_myapp_session', 'secret' => 'Keep it secret; keep it safe.', 'no_cookies' => true, - 'no_hidden' => true } + 'no_hidden' => true, + 'session_http_only' => true + } end def self.cookies @@ -149,6 +151,48 @@ def test_close_marshals_and_writes_cookie assert_equal 1, session.cgi.output_cookies.size cookie = session.cgi.output_cookies.first assert_cookie cookie, cookie_value(:flashed) + assert_http_only_cookie cookie + assert_secure_cookie cookie, false + end + end + + def test_writes_non_secure_cookie_by_default + set_cookie! cookie_value(:typical) + new_session do |session| + session['flash'] = {} + session.close + cookie = session.cgi.output_cookies.first + assert_secure_cookie cookie,false + end + end + + def test_writes_secure_cookie + set_cookie! cookie_value(:typical) + new_session('session_secure'=>true) do |session| + session['flash'] = {} + session.close + cookie = session.cgi.output_cookies.first + assert_secure_cookie cookie + end + end + + def test_http_only_cookie_by_default + set_cookie! cookie_value(:typical) + new_session do |session| + session['flash'] = {} + session.close + cookie = session.cgi.output_cookies.first + assert_http_only_cookie cookie + end + end + + def test_overides_http_only_cookie + set_cookie! cookie_value(:typical) + new_session('session_http_only'=>false) do |session| + session['flash'] = {} + session.close + cookie = session.cgi.output_cookies.first + assert_http_only_cookie cookie, false end end @@ -195,6 +239,13 @@ def assert_cookie(cookie, value = nil, expires = nil, message = nil) assert_equal expires, cookie.expires ? cookie.expires.to_date : cookie.expires, message end + def assert_secure_cookie(cookie,value=true) + assert cookie.secure==value + end + + def assert_http_only_cookie(cookie,value=true) + assert cookie.http_only==value + end def cookies(*which) self.class.cookies.values_at(*which) diff --git a/vendor/rails/actionpack/test/controller/translation_test.rb b/vendor/rails/actionpack/test/controller/translation_test.rb new file mode 100644 index 0000000..0bf61a6 --- /dev/null +++ b/vendor/rails/actionpack/test/controller/translation_test.rb @@ -0,0 +1,26 @@ +require 'abstract_unit' + +# class TranslatingController < ActionController::Base +# end + +class TranslationControllerTest < Test::Unit::TestCase + def setup + @controller = ActionController::Base.new + end + + def test_action_controller_base_responds_to_translate + assert @controller.respond_to?(:translate) + end + + def test_action_controller_base_responds_to_t + assert @controller.respond_to?(:t) + end + + def test_action_controller_base_responds_to_localize + assert @controller.respond_to?(:localize) + end + + def test_action_controller_base_responds_to_l + assert @controller.respond_to?(:l) + end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/test/template/form_tag_helper_test.rb b/vendor/rails/actionpack/test/template/form_tag_helper_test.rb index 9b41ff8..6473d01 100644 --- a/vendor/rails/actionpack/test/template/form_tag_helper_test.rb +++ b/vendor/rails/actionpack/test/template/form_tag_helper_test.rb @@ -223,14 +223,14 @@ def test_stringify_symbol_keys def test_submit_tag assert_dom_equal( - %(), + %(), submit_tag("Save", :disable_with => "Saving...", :onclick => "alert('hello!')") ) end def test_submit_tag_with_no_onclick_options assert_dom_equal( - %(), + %(), submit_tag("Save", :disable_with => "Saving...") ) end @@ -241,6 +241,13 @@ def test_submit_tag_with_confirmation submit_tag("Save", :confirm => "Are you sure?") ) end + + def test_image_submit_tag_with_confirmation + assert_dom_equal( + %(), + image_submit_tag("save.gif", :confirm => "Are you sure?") + ) + end def test_pass assert_equal 1, 1 diff --git a/vendor/rails/activerecord/CHANGELOG b/vendor/rails/activerecord/CHANGELOG index 58d0669..ff2b064 100644 --- a/vendor/rails/activerecord/CHANGELOG +++ b/vendor/rails/activerecord/CHANGELOG @@ -1,5 +1,17 @@ *Edge* +* MySQL: cope with quirky default values for not-null text columns. #1043 [Frederick Cheung] + +* Multiparameter attributes skip time zone conversion for time-only columns [#1030 state:resolved] [Geoff Buesing] + +* Base.skip_time_zone_conversion_for_attributes uses class_inheritable_accessor, so that subclasses don't overwrite Base [#346 state:resolved] [miloops] + +* Added find_last_by dynamic finder #762 [miloops] + +* Internal API: configurable association options and build_association method for reflections so plugins may extend and override. #985 [Hongli Lai] + +* Changed benchmarks to be reported in milliseconds [DHH] + * Connection pooling. #936 [Nick Sieger] * Merge scoped :joins together instead of overwriting them. May expose scoping bugs in your code! #501 [Andrew White] @@ -14,24 +26,6 @@ * Fixed that create database statements would always include "DEFAULT NULL" (Nick Sieger) [#334] -* Add :accessible option to associations for allowing (opt-in) mass assignment. #474. [David Dollar] Example : - - class Post < ActiveRecord::Base - belongs_to :author, :accessible => true - has_many :comments, :accessible => true - end - - post = Post.create({ - :title => 'Accessible Attributes', - :author => { :name => 'David Dollar' }, - :comments => [ - { :body => 'First Post!' }, - { :body => 'Nested Hashes are great!' } - ] - }) - - post.comments << { :body => 'Another Comment' } - * Add :tokenizer option to validates_length_of to specify how to split up the attribute string. #507. [David Lowenfels] Example : # Ensure essay contains at least 100 words. diff --git a/vendor/rails/activerecord/lib/active_record/aggregations.rb b/vendor/rails/activerecord/lib/active_record/aggregations.rb index a5d3a50..9eee7f2 100644 --- a/vendor/rails/activerecord/lib/active_record/aggregations.rb +++ b/vendor/rails/activerecord/lib/active_record/aggregations.rb @@ -10,10 +10,10 @@ def clear_aggregation_cache #:nodoc: end unless self.new_record? end - # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes + # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is] - # composed of [an] address". Each call to the macro adds a description of how the value objects are created from the - # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object) + # composed of [an] address". Each call to the macro adds a description of how the value objects are created from the + # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object) # and how it can be turned back into attributes (when the entity is saved to the database). Example: # # class Customer < ActiveRecord::Base @@ -30,10 +30,10 @@ def clear_aggregation_cache #:nodoc: # class Money # include Comparable # attr_reader :amount, :currency - # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } - # - # def initialize(amount, currency = "USD") - # @amount, @currency = amount, currency + # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } + # + # def initialize(amount, currency = "USD") + # @amount, @currency = amount, currency # end # # def exchange_to(other_currency) @@ -56,19 +56,19 @@ def clear_aggregation_cache #:nodoc: # # class Address # attr_reader :street, :city - # def initialize(street, city) - # @street, @city = street, city + # def initialize(street, city) + # @street, @city = street, city # end # - # def close_to?(other_address) - # city == other_address.city + # def close_to?(other_address) + # city == other_address.city # end # # def ==(other_address) # city == other_address.city && street == other_address.street # end # end - # + # # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the # composition the same as the attribute's name, it will be the only way to access that attribute. That's the case with our # +balance+ attribute. You interact with the value objects just like you would any other attribute, though: @@ -87,8 +87,8 @@ def clear_aggregation_cache #:nodoc: # customer.address_city = "Copenhagen" # customer.address # => Address.new("Hyancintvej", "Copenhagen") # customer.address = Address.new("May Street", "Chicago") - # customer.address_street # => "May Street" - # customer.address_city # => "Chicago" + # customer.address_street # => "May Street" + # customer.address_city # => "Chicago" # # == Writing value objects # @@ -103,12 +103,51 @@ def clear_aggregation_cache #:nodoc: # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been # changed through means other than the writer method. # - # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to + # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to # change it afterwards will result in a ActiveSupport::FrozenObjectError. - # + # # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable # + # == Custom constructors and converters + # + # By default value objects are initialized by calling the new constructor of the value class passing each of the + # mapped attributes, in the order specified by the :mapping option, as arguments. If the value class doesn't support + # this convention then +composed_of+ allows a custom constructor to be specified. + # + # When a new value is assigned to the value object the default assumption is that the new value is an instance of the value + # class. Specifying a custom converter allows the new value to be automatically converted to an instance of value class if + # necessary. + # + # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be aggregated using the + # NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor for the value class is called +create+ and it + # expects a CIDR address string as a parameter. New values can be assigned to the value object using either another + # NetAddr::CIDR object, a string or an array. The :constructor and :converter options can be used to + # meet these requirements: + # + # class NetworkResource < ActiveRecord::Base + # composed_of :cidr, + # :class_name => 'NetAddr::CIDR', + # :mapping => [ %w(network_address network), %w(cidr_range bits) ], + # :allow_nil => true, + # :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") }, + # :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) } + # end + # + # # This calls the :constructor + # network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24) + # + # # These assignments will both use the :converter + # network_resource.cidr = [ '192.168.2.1', 8 ] + # network_resource.cidr = '192.168.0.1/24' + # + # # This assignment won't use the :converter as the value is already an instance of the value class + # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8') + # + # # Saving and then reloading will use the :constructor on reload + # network_resource.save + # network_resource.reload + # # == Finding records by a value object # # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database by specifying an instance @@ -122,47 +161,71 @@ module ClassMethods # composed_of :address adds address and address=(new_address) methods. # # Options are: - # * :class_name - specify the class name of the association. Use it only if that name can't be inferred + # * :class_name - Specifies the class name of the association. Use it only if that name can't be inferred # from the part id. So composed_of :address will by default be linked to the Address class, but # if the real class name is CompanyAddress, you'll have to specify it with this option. - # * :mapping - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name - # to a constructor parameter on the value class. - # * :allow_nil - specifies that the aggregate object will not be instantiated when all mapped - # attributes are +nil+. Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes. + # * :mapping - Specifies the mapping of entity attributes to attributes of the value object. Each mapping + # is represented as an array where the first item is the name of the entity attribute and the second item is the + # name the attribute in the value object. The order in which mappings are defined determine the order in which + # attributes are sent to the value class constructor. + # * :allow_nil - Specifies that the value object will not be instantiated when all mapped + # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all mapped attributes. # This defaults to +false+. - # - # An optional block can be passed to convert the argument that is passed to the writer method into an instance of - # :class_name. The block will only be called if the argument is not already an instance of :class_name. + # * :constructor - A symbol specifying the name of the constructor method or a Proc that is called to + # initialize the value object. The constructor is passed all of the mapped attributes, in the order that they + # are defined in the :mapping option, as arguments and uses them to instantiate a :class_name object. + # The default is :new. + # * :converter - A symbol specifying the name of a class method of :class_name or a Proc that is + # called when a new value is assigned to the value object. The converter is passed the single value that is used + # in the assignment and is only called if the new value is not an instance of :class_name. # # Option examples: # composed_of :temperature, :mapping => %w(reading celsius) - # composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money } + # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money } # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] # composed_of :gps_location # composed_of :gps_location, :allow_nil => true + # composed_of :ip_address, + # :class_name => 'IPAddr', + # :mapping => %w(ip to_i), + # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) }, + # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) } # def composed_of(part_id, options = {}, &block) - options.assert_valid_keys(:class_name, :mapping, :allow_nil) + options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) name = part_id.id2name - class_name = options[:class_name] || name.camelize - mapping = options[:mapping] || [ name, name ] + class_name = options[:class_name] || name.camelize + mapping = options[:mapping] || [ name, name ] mapping = [ mapping ] unless mapping.first.is_a?(Array) - allow_nil = options[:allow_nil] || false + allow_nil = options[:allow_nil] || false + constructor = options[:constructor] || :new + converter = options[:converter] || block + + ActiveSupport::Deprecation.warn('The conversion block has been deprecated, use the :converter option instead.', caller) if block_given? + + reader_method(name, class_name, mapping, allow_nil, constructor) + writer_method(name, class_name, mapping, allow_nil, converter) - reader_method(name, class_name, mapping, allow_nil) - writer_method(name, class_name, mapping, allow_nil, block) - create_reflection(:composed_of, part_id, options, self) end private - def reader_method(name, class_name, mapping, allow_nil) + def reader_method(name, class_name, mapping, allow_nil, constructor) module_eval do define_method(name) do |*args| force_reload = args.first || false if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) - instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)})) + attrs = mapping.collect {|pair| read_attribute(pair.first)} + object = case constructor + when Symbol + class_name.constantize.send(constructor, *attrs) + when Proc, Method + constructor.call(*attrs) + else + raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.' + end + instance_variable_set("@#{name}", object) end instance_variable_get("@#{name}") end @@ -170,14 +233,23 @@ def reader_method(name, class_name, mapping, allow_nil) end - def writer_method(name, class_name, mapping, allow_nil, conversion) + def writer_method(name, class_name, mapping, allow_nil, converter) module_eval do define_method("#{name}=") do |part| if part.nil? && allow_nil mapping.each { |pair| self[pair.first] = nil } instance_variable_set("@#{name}", nil) else - part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil? + unless part.is_a?(class_name.constantize) || converter.nil? + part = case converter + when Symbol + class_name.constantize.send(converter, part) + when Proc, Method + converter.call(part) + else + raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.' + end + end mapping.each { |pair| self[pair.first] = part.send(pair.last) } instance_variable_set("@#{name}", part.freeze) end diff --git a/vendor/rails/activerecord/lib/active_record/association_preload.rb b/vendor/rails/activerecord/lib/active_record/association_preload.rb index 61fa34a..c60850f 100644 --- a/vendor/rails/activerecord/lib/active_record/association_preload.rb +++ b/vendor/rails/activerecord/lib/active_record/association_preload.rb @@ -95,7 +95,7 @@ def preload_has_and_belongs_to_many_association(records, reflection, preload_opt records.each {|record| record.send(reflection.name).loaded} options = reflection.options - conditions = "t0.#{reflection.primary_key_name} IN (?)" + conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" conditions << append_conditions(options, preload_options) associated_records = reflection.klass.find(:all, :conditions => [conditions, ids], @@ -222,8 +222,6 @@ def preload_belongs_to_association(records, reflection, preload_options={}) table_name = klass.quoted_table_name primary_key = klass.primary_key - conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)" - conditions << append_conditions(options, preload_options) column_type = klass.columns.detect{|c| c.name == primary_key}.type ids = id_map.keys.uniq.map do |id| if column_type == :integer @@ -234,6 +232,8 @@ def preload_belongs_to_association(records, reflection, preload_options={}) id end end + conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}" + conditions << append_conditions(options, preload_options) associated_records = klass.find(:all, :conditions => [conditions, ids], :include => options[:include], :select => options[:select], @@ -248,10 +248,10 @@ def find_associated_records(ids, reflection, preload_options) table_name = reflection.klass.quoted_table_name if interface = reflection.options[:as] - conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'" + conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'" else foreign_key = reflection.primary_key_name - conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)" + conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}" end conditions << append_conditions(options, preload_options) @@ -277,6 +277,9 @@ def append_conditions(options, preload_options) sql end + def in_or_equals_for_ids(ids) + ids.size > 1 ? "IN (?)" : "= ?" + end end end end diff --git a/vendor/rails/activerecord/lib/active_record/associations.rb b/vendor/rails/activerecord/lib/active_record/associations.rb index 6405071..5d91315 100755 --- a/vendor/rails/activerecord/lib/active_record/associations.rb +++ b/vendor/rails/activerecord/lib/active_record/associations.rb @@ -739,9 +739,6 @@ module ClassMethods # If true, all the associated objects are readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. true by default. - # [:accessible] - # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). - # Option examples: # has_many :comments, :order => "posted_on" # has_many :comments, :include => :author @@ -855,8 +852,6 @@ def has_many(association_id, options = {}, &extension) # If true, the associated object is readonly through the association. # [:validate] # If false, don't validate the associated object when saving the parent object. +false+ by default. - # [:accessible] - # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card @@ -973,8 +968,6 @@ def has_one(association_id, options = {}) # If true, the associated object is readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. +false+ by default. - # [:accessible] - # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # belongs_to :firm, :foreign_key => "client_of" @@ -1190,8 +1183,6 @@ def belongs_to(association_id, options = {}) # If true, all the associated objects are readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. +true+ by default. - # [:accessible<] - # Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # has_and_belongs_to_many :projects @@ -1246,7 +1237,7 @@ def association_accessor_methods(reflection, association_proxy_class) association = instance_variable_get(ivar) if instance_variable_defined?(ivar) - if association.nil? || force_reload + if association.nil? || !association.loaded? || force_reload association = association_proxy_class.new(self, reflection) retval = association.reload if retval.nil? and association_proxy_class == BelongsToAssociation @@ -1266,8 +1257,6 @@ def association_accessor_methods(reflection, association_proxy_class) association = association_proxy_class.new(self, reflection) end - new_value = reflection.klass.new(new_value) if reflection.options[:accessible] && new_value.is_a?(Hash) - if association_proxy_class == HasOneThroughAssociation association.create_through_record(new_value) self.send(reflection.name, new_value) @@ -1277,6 +1266,13 @@ def association_accessor_methods(reflection, association_proxy_class) end end + if association_proxy_class == BelongsToAssociation + define_method("#{reflection.primary_key_name}=") do |target_id| + instance_variable_get(ivar).reset if instance_variable_defined?(ivar) + write_attribute(reflection.primary_key_name, target_id) + end + end + define_method("set_#{reflection.name}_target") do |target| return if target.nil? and association_proxy_class == BelongsToAssociation association = association_proxy_class.new(self, reflection) @@ -1509,29 +1505,35 @@ def configure_dependency_for_belongs_to(reflection) end end - def create_has_many_reflection(association_id, options, &extension) - options.assert_valid_keys( - :class_name, :table_name, :foreign_key, :primary_key, - :dependent, - :select, :conditions, :include, :order, :group, :limit, :offset, - :as, :through, :source, :source_type, - :uniq, - :finder_sql, :counter_sql, - :before_add, :after_add, :before_remove, :after_remove, - :extend, :readonly, - :validate, :accessible - ) + mattr_accessor :valid_keys_for_has_many_association + @@valid_keys_for_has_many_association = [ + :class_name, :table_name, :foreign_key, :primary_key, + :dependent, + :select, :conditions, :include, :order, :group, :limit, :offset, + :as, :through, :source, :source_type, + :uniq, + :finder_sql, :counter_sql, + :before_add, :after_add, :before_remove, :after_remove, + :extend, :readonly, + :validate + ] + def create_has_many_reflection(association_id, options, &extension) + options.assert_valid_keys(valid_keys_for_has_many_association) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) create_reflection(:has_many, association_id, options, self) end - def create_has_one_reflection(association_id, options) - options.assert_valid_keys( - :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate, :primary_key, :accessible - ) + mattr_accessor :valid_keys_for_has_one_association + @@valid_keys_for_has_one_association = [ + :class_name, :foreign_key, :remote, :select, :conditions, :order, + :include, :dependent, :counter_cache, :extend, :as, :readonly, + :validate, :primary_key + ] + def create_has_one_reflection(association_id, options) + options.assert_valid_keys(valid_keys_for_has_one_association) create_reflection(:has_one, association_id, options, self) end @@ -1542,12 +1544,15 @@ def create_has_one_through_reflection(association_id, options) create_reflection(:has_one, association_id, options, self) end - def create_belongs_to_reflection(association_id, options) - options.assert_valid_keys( - :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent, - :counter_cache, :extend, :polymorphic, :readonly, :validate, :accessible - ) + mattr_accessor :valid_keys_for_belongs_to_association + @@valid_keys_for_belongs_to_association = [ + :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, + :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly, + :validate + ] + def create_belongs_to_reflection(association_id, options) + options.assert_valid_keys(valid_keys_for_belongs_to_association) reflection = create_reflection(:belongs_to, association_id, options, self) if options[:polymorphic] @@ -1565,7 +1570,7 @@ def create_has_and_belongs_to_many_reflection(association_id, options, &extensio :finder_sql, :delete_sql, :insert_sql, :before_add, :after_add, :before_remove, :after_remove, :extend, :readonly, - :validate, :accessible + :validate ) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) diff --git a/vendor/rails/activerecord/lib/active_record/associations/association_collection.rb b/vendor/rails/activerecord/lib/active_record/associations/association_collection.rb index 168443e..afb817f 100644 --- a/vendor/rails/activerecord/lib/active_record/associations/association_collection.rb +++ b/vendor/rails/activerecord/lib/active_record/associations/association_collection.rb @@ -110,8 +110,6 @@ def <<(*records) @owner.transaction do flatten_deeper(records).each do |record| - record = @reflection.klass.new(record) if @reflection.options[:accessible] && record.is_a?(Hash) - raise_on_type_mismatch(record) add_record_to_target_with_callbacks(record) do |r| result &&= insert_record(record) unless @owner.new_record? @@ -240,6 +238,8 @@ def create!(attrs = {}) def size if @owner.new_record? || (loaded? && !@reflection.options[:uniq]) @target.size + elsif !loaded? && @reflection.options[:group] + load_target.size elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array) unsaved_records = @target.select { |r| r.new_record? } unsaved_records.size + count_records @@ -286,10 +286,6 @@ def uniq(collection = self) # Replace this collection with +other_array+ # This will perform a diff and delete/add only records that have changed. def replace(other_array) - other_array.map! do |val| - val.is_a?(Hash) ? @reflection.klass.new(val) : val - end if @reflection.options[:accessible] - other_array.each { |val| raise_on_type_mismatch(val) } load_target @@ -377,7 +373,9 @@ def find_target def create_record(attrs) attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) ensure_owner_is_not_new - record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) } + record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do + @reflection.build_association(attrs) + end if block_given? add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } else @@ -387,7 +385,7 @@ def create_record(attrs) def build_record(attrs) attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - record = @reflection.klass.new(attrs) + record = @reflection.build_association(attrs) if block_given? add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } else diff --git a/vendor/rails/activerecord/lib/active_record/associations/belongs_to_association.rb b/vendor/rails/activerecord/lib/active_record/associations/belongs_to_association.rb index 7c28cbd..f05c6be 100644 --- a/vendor/rails/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/vendor/rails/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -2,11 +2,11 @@ module ActiveRecord module Associations class BelongsToAssociation < AssociationProxy #:nodoc: def create(attributes = {}) - replace(@reflection.klass.create(attributes)) + replace(@reflection.create_association(attributes)) end def build(attributes = {}) - replace(@reflection.klass.new(attributes)) + replace(@reflection.build_association(attributes)) end def replace(record) diff --git a/vendor/rails/activerecord/lib/active_record/associations/has_many_association.rb b/vendor/rails/activerecord/lib/active_record/associations/has_many_association.rb index dda2266..3b2f306 100644 --- a/vendor/rails/activerecord/lib/active_record/associations/has_many_association.rb +++ b/vendor/rails/activerecord/lib/active_record/associations/has_many_association.rb @@ -17,7 +17,10 @@ def owner_quoted_id # Returns the number of records in this collection. # # If the association has a counter cache it gets that value. Otherwise - # a count via SQL is performed, bounded to :limit if there's one. + # it will attempt to do a count via SQL, bounded to :limit if + # there's one. Some configuration options like :group make it impossible + # to do a SQL count, in those cases the array count will be used. + # # That does not depend on whether the collection has already been loaded # or not. The +size+ method is the one that takes the loaded flag into # account and delegates to +count_records+ if needed. diff --git a/vendor/rails/activerecord/lib/active_record/associations/has_many_through_association.rb b/vendor/rails/activerecord/lib/active_record/associations/has_many_through_association.rb index 84fa900..ebd2bf7 100644 --- a/vendor/rails/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/vendor/rails/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -10,14 +10,14 @@ def initialize(owner, reflection) def create!(attrs = nil) @reflection.klass.transaction do - self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! } : @reflection.klass.create!) + self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!) object end end def create(attrs = nil) @reflection.klass.transaction do - self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create } : @reflection.klass.create) + self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association) object end end @@ -47,8 +47,9 @@ def insert_record(record, force=true) return false unless record.save end end - klass = @reflection.through_reflection.klass - @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { klass.create! } + through_reflection = @reflection.through_reflection + klass = through_reflection.klass + @owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { through_reflection.create_association! } end # TODO - add dependent option support diff --git a/vendor/rails/activerecord/lib/active_record/associations/has_one_association.rb b/vendor/rails/activerecord/lib/active_record/associations/has_one_association.rb index 1873325..c92ef5c 100644 --- a/vendor/rails/activerecord/lib/active_record/associations/has_one_association.rb +++ b/vendor/rails/activerecord/lib/active_record/associations/has_one_association.rb @@ -7,15 +7,21 @@ def initialize(owner, reflection) end def create(attrs = {}, replace_existing = true) - new_record(replace_existing) { |klass| klass.create(attrs) } + new_record(replace_existing) do |reflection| + reflection.create_association(attrs) + end end def create!(attrs = {}, replace_existing = true) - new_record(replace_existing) { |klass| klass.create!(attrs) } + new_record(replace_existing) do |reflection| + reflection.create_association!(attrs) + end end def build(attrs = {}, replace_existing = true) - new_record(replace_existing) { |klass| klass.new(attrs) } + new_record(replace_existing) do |reflection| + reflection.build_association(attrs) + end end def replace(obj, dont_save = false) @@ -91,7 +97,9 @@ def new_record(replace_existing) # instance. Otherwise, if the target has not previously been loaded # elsewhere, the instance we create will get orphaned. load_target if replace_existing - record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { yield @reflection.klass } + record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do + yield @reflection + end if replace_existing replace(record, true) diff --git a/vendor/rails/activerecord/lib/active_record/attribute_methods.rb b/vendor/rails/activerecord/lib/active_record/attribute_methods.rb index 0a1baff..020da01 100644 --- a/vendor/rails/activerecord/lib/active_record/attribute_methods.rb +++ b/vendor/rails/activerecord/lib/active_record/attribute_methods.rb @@ -10,7 +10,7 @@ def self.included(base) base.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT base.cattr_accessor :time_zone_aware_attributes, :instance_writer => false base.time_zone_aware_attributes = false - base.cattr_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false + base.class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false base.skip_time_zone_conversion_for_attributes = [] end diff --git a/vendor/rails/activerecord/lib/active_record/base.rb b/vendor/rails/activerecord/lib/active_record/base.rb index 3419aad..b20da51 100755 --- a/vendor/rails/activerecord/lib/active_record/base.rb +++ b/vendor/rails/activerecord/lib/active_record/base.rb @@ -274,7 +274,7 @@ def initialize(errors) # == Dynamic attribute-based finders # # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects by simple queries without turning to SQL. They work by - # appending the name of an attribute to find_by_ or find_all_by_, so you get finders like Person.find_by_user_name, + # appending the name of an attribute to find_by_, find_last_by_, or find_all_by_, so you get finders like Person.find_by_user_name, # Person.find_all_by_last_name, and Payment.find_by_transaction_id. So instead of writing # Person.find(:first, :conditions => ["user_name = ?", user_name]), you just do Person.find_by_user_name(user_name). # And instead of writing Person.find(:all, :conditions => ["last_name = ?", last_name]), you just do Person.find_all_by_last_name(last_name). @@ -287,6 +287,7 @@ def initialize(errors) # It's even possible to use all the additional parameters to find. For example, the full interface for Payment.find_all_by_amount # is actually Payment.find_all_by_amount(amount, options). And the full interface to Person.find_by_user_name is # actually Person.find_by_user_name(user_name, options). So you could call Payment.find_all_by_amount(50, :order => "created_on"). + # Also you may call Payment.find_last_by_amount(amount, options) returning the last record matching that amount and options. # # The same dynamic finder style can be used to create the object if it doesn't already exist. This dynamic finder is called with # find_or_create_by_ and will return the object if it already exists and otherwise creates it, then returns it. Protected attributes won't be set unless they are given in a block. For example: @@ -768,10 +769,24 @@ def destroy(id) # :order => 'created_at', :limit => 5 ) def update_all(updates, conditions = nil, options = {}) sql = "UPDATE #{quoted_table_name} SET #{sanitize_sql_for_assignment(updates)} " + scope = scope(:find) - add_conditions!(sql, conditions, scope) - add_order!(sql, options[:order], nil) - add_limit!(sql, options, nil) + + select_sql = "" + add_conditions!(select_sql, conditions, scope) + + if options.has_key?(:limit) || (scope && scope[:limit]) + # Only take order from scope if limit is also provided by scope, this + # is useful for updating a has_many association with a limit. + add_order!(select_sql, options[:order], scope) + + add_limit!(select_sql, options, scope) + sql.concat(connection.limited_update_conditions(select_sql, quoted_table_name, connection.quote_column_name(primary_key))) + else + add_order!(select_sql, options[:order], nil) + sql.concat(select_sql) + end + connection.update(sql, "#{name} Update") end @@ -1313,7 +1328,7 @@ def benchmark(title, log_level = Logger::DEBUG, use_silence = true) if logger && logger.level <= log_level result = nil seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield } - logger.add(log_level, "#{title} (#{'%.5f' % seconds})") + logger.add(log_level, "#{title} (#{'%.1f' % (seconds * 1000)}ms)") result else yield @@ -2137,7 +2152,7 @@ def expand_range_bind_variables(bind_vars) #:nodoc: end def quote_bound_value(value) #:nodoc: - if value.respond_to?(:map) && !value.is_a?(String) + if value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? connection.quote(nil) else @@ -2715,7 +2730,7 @@ def assign_multiparameter_attributes(pairs) end def instantiate_time_object(name, values) - if self.class.time_zone_aware_attributes && !self.class.skip_time_zone_conversion_for_attributes.include?(name.to_sym) + if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) Time.zone.local(*values) else Time.time_with_datetime_fallback(@@default_timezone, *values) diff --git a/vendor/rails/activerecord/lib/active_record/calculations.rb b/vendor/rails/activerecord/lib/active_record/calculations.rb index a675af4..80992dd 100644 --- a/vendor/rails/activerecord/lib/active_record/calculations.rb +++ b/vendor/rails/activerecord/lib/active_record/calculations.rb @@ -217,7 +217,7 @@ def construct_calculation_sql(operation, column_name, options) #:nodoc: sql << " ORDER BY #{options[:order]} " if options[:order] add_limit!(sql, options, scope) - sql << ') AS #{aggregate_alias}_subquery' if use_workaround + sql << ") AS #{aggregate_alias}_subquery" if use_workaround sql end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index aaf9e2e..8fc89de 100644 --- a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -153,6 +153,10 @@ def case_sensitive_equality_operator "=" end + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})" + end + protected # Returns an array of record hashes with the column names as keys and # column values as values. diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 75032ef..58992f9 100644 --- a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -40,6 +40,10 @@ def number? type == :integer || type == :float || type == :decimal end + def has_default? + !default.nil? + end + # Returns the Ruby class that corresponds to the abstract data type. def klass case type @@ -252,6 +256,10 @@ def simplified_type(field_type) class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc: end + # Abstract representation of a column definition. Instances of this type + # are typically created by methods in TableDefinition, and added to the + # +columns+ attribute of said TableDefinition object, in order to be used + # for generating a number of table creation or table changing SQL statements. class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc: def sql_type @@ -275,9 +283,29 @@ def add_column_options!(sql, options) end end - # Represents a SQL table in an abstract way. - # Columns are stored as a ColumnDefinition in the +columns+ attribute. + # Represents the schema of an SQL table in an abstract way. This class + # provides methods for manipulating the schema representation. + # + # Inside migration files, the +t+ object in +create_table+ and + # +change_table+ is actually of this type: + # + # class SomeMigration < ActiveRecord::Migration + # def self.up + # create_table :foo do |t| + # puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition" + # end + # end + # + # def self.down + # ... + # end + # end + # + # The table definitions + # The Columns are stored as a ColumnDefinition in the +columns+ attribute. class TableDefinition + # An array of ColumnDefinition objects, representing the column changes + # that have been defined. attr_accessor :columns def initialize(base) @@ -321,6 +349,12 @@ def [](name) # * :scale - # Specifies the scale for a :decimal column. # + # For clarity's sake: the precision is the number of significant digits, + # while the scale is the number of digits that can be stored following + # the decimal point. For example, the number 123.45 has a precision of 5 + # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can + # range from -999.99 to 999.99. + # # Please be aware of different RDBMS implementations behavior with # :decimal columns: # * The SQL standard says the default scale should be 0, :scale <= @@ -374,6 +408,10 @@ def [](name) # td.column(:huge_integer, :decimal, :precision => 30) # # => huge_integer DECIMAL(30) # + # # Defines a column with a database-specific type. + # td.column(:foo, 'polygon') + # # => foo polygon + # # == Short-hand examples # # Instead of calling +column+ directly, you can also work with the short-hand definitions for the default types. diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 14dde57..7c37916 100755 --- a/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -147,10 +147,10 @@ def decrement_open_transactions @open_transactions -= 1 end - def log_info(sql, name, runtime) + def log_info(sql, name, seconds) if @logger && @logger.debug? - name = "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})" - @logger.debug format_log_entry(name, sql.squeeze(' ')) + name = "#{name.nil? ? "SQL" : name} (#{sprintf("%.1f", seconds * 1000)}ms)" + @logger.debug(format_log_entry(name, sql.squeeze(' '))) end end diff --git a/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index f1d1369..a26fd02 100644 --- a/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -80,7 +80,7 @@ class MysqlColumn < Column #:nodoc: def extract_default(default) if type == :binary || type == :text if default.blank? - nil + return null ? nil : '' else raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" end @@ -91,6 +91,11 @@ def extract_default(default) end end + def has_default? + return false if type == :binary || type == :text #mysql forbids defaults on blob and text columns + super + end + private def simplified_type(field_type) return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)") @@ -523,6 +528,10 @@ def case_sensitive_equality_operator "= BINARY" end + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + where_sql + end + private def connect @connection.reconnect = true if @connection.respond_to?(:reconnect=) diff --git a/vendor/rails/activerecord/lib/active_record/dirty.rb b/vendor/rails/activerecord/lib/active_record/dirty.rb index 7e246e6..ae57379 100644 --- a/vendor/rails/activerecord/lib/active_record/dirty.rb +++ b/vendor/rails/activerecord/lib/active_record/dirty.rb @@ -34,8 +34,10 @@ module ActiveRecord # person.name << 'by' # person.name_change # => ['uncle bob', 'uncle bobby'] module Dirty + DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was'] + def self.included(base) - base.attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' + base.attribute_method_suffix *DIRTY_SUFFIXES base.alias_method_chain :write_attribute, :dirty base.alias_method_chain :save, :dirty base.alias_method_chain :save!, :dirty @@ -44,6 +46,8 @@ def self.included(base) base.superclass_delegating_accessor :partial_updates base.partial_updates = true + + base.send(:extend, ClassMethods) end # Do any attributes have unsaved changes? @@ -161,5 +165,19 @@ def field_changed?(attr, old, value) old != value end + module ClassMethods + def self.extended(base) + base.metaclass.alias_method_chain(:alias_attribute, :dirty) + end + + def alias_attribute_with_dirty(new_name, old_name) + alias_attribute_without_dirty(new_name, old_name) + DIRTY_SUFFIXES.each do |suffix| + module_eval <<-STR, __FILE__, __LINE__+1 + def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end + STR + end + end + end end end diff --git a/vendor/rails/activerecord/lib/active_record/dynamic_finder_match.rb b/vendor/rails/activerecord/lib/active_record/dynamic_finder_match.rb index b105b91..f4a5712 100644 --- a/vendor/rails/activerecord/lib/active_record/dynamic_finder_match.rb +++ b/vendor/rails/activerecord/lib/active_record/dynamic_finder_match.rb @@ -8,7 +8,8 @@ def self.match(method) def initialize(method) @finder = :find_initial case method.to_s - when /^find_(all_by|by)_([_a-zA-Z]\w*)$/ + when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/ + @finder = :find_last if $1 == 'last_by' @finder = :find_every if $1 == 'all_by' names = $2 when /^find_by_([_a-zA-Z]\w*)\!$/ diff --git a/vendor/rails/activerecord/lib/active_record/locale/en-US.yml b/vendor/rails/activerecord/lib/active_record/locale/en-US.yml index 8148f31..421f0eb 100644 --- a/vendor/rails/activerecord/lib/active_record/locale/en-US.yml +++ b/vendor/rails/activerecord/lib/active_record/locale/en-US.yml @@ -25,9 +25,30 @@ en-US: even: "must be even" # Append your own errors here or at the model/attributes scope. + # You can define own errors for models or model attributes. + # The values :model, :attribute and :value are always available for interpolation. + # + # For example, + # models: + # user: + # blank: "This is a custom blank message for {{model}}: {{attribute}}" + # attributes: + # login: + # blank: "This is a custom blank message for User login" + # Will define custom blank validation message for User model and + # custom blank validation message for login attribute of User model. models: - # Overrides default messages - - attributes: - # Overrides model and default messages. + + # Translate model names. Used in Model.human_name(). + #models: + # For example, + # user: "Dude" + # will translate User model name to "Dude" + + # Translate model attribute names. Used in Model.human_attribute_name(attribute). + #attributes: + # For example, + # user: + # login: "Handle" + # will translate User attribute "login" as "Handle" diff --git a/vendor/rails/activerecord/lib/active_record/reflection.rb b/vendor/rails/activerecord/lib/active_record/reflection.rb index 935b193..a1b498e 100644 --- a/vendor/rails/activerecord/lib/active_record/reflection.rb +++ b/vendor/rails/activerecord/lib/active_record/reflection.rb @@ -129,10 +129,45 @@ class AggregateReflection < MacroReflection #:nodoc: # Holds all the meta-data about an association as it was specified in the Active Record class. class AssociationReflection < MacroReflection #:nodoc: + # Returns the target association's class: + # + # class Author < ActiveRecord::Base + # has_many :books + # end + # + # Author.reflect_on_association(:books).klass + # # => Book + # + # Note: do not call +klass.new+ or +klass.create+ to instantiate + # a new association object. Use +build_association+ or +create_association+ + # instead. This allows plugins to hook into association object creation. def klass @klass ||= active_record.send(:compute_type, class_name) end + # Returns a new, unsaved instance of the associated class. +options+ will + # be passed to the class's constructor. + def build_association(*options) + klass.new(*options) + end + + # Creates a new instance of the associated class, and immediates saves it + # with ActiveRecord::Base#save. +options+ will be passed to the class's + # creation method. Returns the newly created object. + def create_association(*options) + klass.create(*options) + end + + # Creates a new instance of the associated class, and immediates saves it + # with ActiveRecord::Base#save!. +options+ will be passed to the class's + # creation method. If the created record doesn't pass validations, then an + # exception will be raised. + # + # Returns the newly created object. + def create_association!(*options) + klass.create!(*options) + end + def table_name @table_name ||= klass.table_name end diff --git a/vendor/rails/activerecord/lib/active_record/schema_dumper.rb b/vendor/rails/activerecord/lib/active_record/schema_dumper.rb index b90ed88..4f96e22 100644 --- a/vendor/rails/activerecord/lib/active_record/schema_dumper.rb +++ b/vendor/rails/activerecord/lib/active_record/schema_dumper.rb @@ -102,7 +102,7 @@ def table(table, stream) spec[:precision] = column.precision.inspect if !column.precision.nil? spec[:scale] = column.scale.inspect if !column.scale.nil? spec[:null] = 'false' if !column.null - spec[:default] = default_string(column.default) if !column.default.nil? + spec[:default] = default_string(column.default) if column.has_default? (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} spec end.compact diff --git a/vendor/rails/activerecord/lib/active_record/validations.rb b/vendor/rails/activerecord/lib/active_record/validations.rb index 577e30e..518b59e 100644 --- a/vendor/rails/activerecord/lib/active_record/validations.rb +++ b/vendor/rails/activerecord/lib/active_record/validations.rb @@ -259,6 +259,8 @@ def to_xml(options={}) end + # Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations. + # # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression). @@ -297,8 +299,6 @@ def to_xml(options={}) # person.save # => true (and person is now saved in the database) # # An Errors object is automatically created for every Active Record. - # - # Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations. module Validations VALIDATIONS = %w( validate validate_on_create validate_on_update ) @@ -313,9 +313,50 @@ def self.included(base) # :nodoc: base.define_callbacks *VALIDATIONS end - # All of the following validations are defined in the class scope of the model that you're interested in validating. - # They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use - # these over the low-level calls to +validate+ and +validate_on_create+ when possible. + # Active Record classes can implement validations in several ways. The highest level, easiest to read, + # and recommended approach is to use the declarative validates_..._of class methods (and + # +validates_associated+) documented below. These are sufficient for most model validations. + # + # Slightly lower level is +validates_each+. It provides some of the same options as the purely declarative + # validation methods, but like all the lower-level approaches it requires manually adding to the errors collection + # when the record is invalid. + # + # At a yet lower level, a model can use the class methods +validate+, +validate_on_create+ and +validate_on_update+ + # to add validation methods or blocks. These are ActiveSupport::Callbacks and follow the same rules of inheritance + # and chaining. + # + # The lowest level style is to define the instance methods +validate+, +validate_on_create+ and +validate_on_update+ + # as documented in ActiveRecord::Validations. + # + # == +validate+, +validate_on_create+ and +validate_on_update+ Class Methods + # + # Calls to these methods add a validation method or block to the class. Again, this approach is recommended + # only when the higher-level methods documented below (validates_..._of and +validates_associated+) are + # insufficient to handle the required validation. + # + # This can be done with a symbol pointing to a method: + # + # class Comment < ActiveRecord::Base + # validate :must_be_friends + # + # def must_be_friends + # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # end + # end + # + # Or with a block which is passed the current record to be validated: + # + # class Comment < ActiveRecord::Base + # validate do |comment| + # comment.must_be_friends + # end + # + # def must_be_friends + # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # end + # end + # + # This usage applies to +validate_on_create+ and +validate_on_update+ as well. module ClassMethods DEFAULT_VALIDATION_OPTIONS = { :on => :save, @@ -329,34 +370,6 @@ module ClassMethods :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=', :odd => 'odd?', :even => 'even?' }.freeze - # Adds a validation method or block to the class. This is useful when - # overriding the +validate+ instance method becomes too unwieldy and - # you're looking for more descriptive declaration of your validations. - # - # This can be done with a symbol pointing to a method: - # - # class Comment < ActiveRecord::Base - # validate :must_be_friends - # - # def must_be_friends - # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) - # end - # end - # - # Or with a block which is passed the current record to be validated: - # - # class Comment < ActiveRecord::Base - # validate do |comment| - # comment.must_be_friends - # end - # - # def must_be_friends - # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) - # end - # end - # - # This usage applies to +validate_on_create+ and +validate_on_update+ as well. - # Validates each attribute against a block. # # class Person < ActiveRecord::Base @@ -509,13 +522,13 @@ def validates_presence_of(*attr_names) # # class Person < ActiveRecord::Base # validates_length_of :first_name, :maximum=>30 - # validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind" + # validates_length_of :last_name, :maximum=>30, :message=>"less than {{count}} if you don't mind" # validates_length_of :fax, :in => 7..32, :allow_nil => true # validates_length_of :phone, :in => 7..32, :allow_blank => true # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" - # validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least %d character" - # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with %d characters... don't play me." - # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least %d words."), :tokenizer => lambda {|str| str.scan(/\w+/) } + # validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least {{count}} character" + # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with {{count}} characters... don't play me." + # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least {{count}} words."), :tokenizer => lambda {|str| str.scan(/\w+/) } # end # # Configuration options: @@ -526,9 +539,9 @@ def validates_presence_of(*attr_names) # * :in - A synonym(or alias) for :within. # * :allow_nil - Attribute may be +nil+; skip validation. # * :allow_blank - Attribute may be blank; skip validation. - # * :too_long - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)"). - # * :too_short - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)"). - # * :wrong_length - The error message if using the :is method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)"). + # * :too_long - The error message if the attribute goes over the maximum (default is: "is too long (maximum is {{count}} characters)"). + # * :too_short - The error message if the attribute goes under the minimum (default is: "is too short (min is {{count}} characters)"). + # * :wrong_length - The error message if using the :is method and the attribute is the wrong size (default is: "is the wrong length (should be {{count}} characters)"). # * :message - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message. # * :on - Specifies when this validation is active (default is :save, other options :create, :update). # * :if - Specifies a method, proc or string to call to determine if the validation should @@ -731,7 +744,7 @@ def validates_format_of(*attr_names) # class Person < ActiveRecord::Base # validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!" # validates_inclusion_of :age, :in => 0..99 - # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %s is not included in the list" + # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list" # end # # Configuration options: @@ -765,7 +778,7 @@ def validates_inclusion_of(*attr_names) # class Person < ActiveRecord::Base # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here" # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60" - # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %s is not allowed" + # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension {{value}} is not allowed" # end # # Configuration options: diff --git a/vendor/rails/activerecord/test/cases/aggregations_test.rb b/vendor/rails/activerecord/test/cases/aggregations_test.rb index 75d1f27..4e0e1c7 100644 --- a/vendor/rails/activerecord/test/cases/aggregations_test.rb +++ b/vendor/rails/activerecord/test/cases/aggregations_test.rb @@ -107,6 +107,45 @@ def test_nil_assignment_results_in_nil customers(:david).gps_location = nil assert_equal nil, customers(:david).gps_location end + + def test_custom_constructor + assert_equal 'Barney GUMBLE', customers(:barney).fullname.to_s + assert_kind_of Fullname, customers(:barney).fullname + end + + def test_custom_converter + customers(:barney).fullname = 'Barnoit Gumbleau' + assert_equal 'Barnoit GUMBLEAU', customers(:barney).fullname.to_s + assert_kind_of Fullname, customers(:barney).fullname + end +end + +class DeprecatedAggregationsTest < ActiveRecord::TestCase + class Person < ActiveRecord::Base; end + + def test_conversion_block_is_deprecated + assert_deprecated 'conversion block has been deprecated' do + Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money } + end + end + + def test_conversion_block_used_when_converter_option_is_nil + assert_deprecated 'conversion block has been deprecated' do + Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money } + end + assert_raise(NoMethodError) { Person.new.balance = 5 } + end + + def test_converter_option_overrides_conversion_block + assert_deprecated 'conversion block has been deprecated' do + Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| Money.new(balance) }) { |balance| balance.to_money } + end + + person = Person.new + assert_nothing_raised { person.balance = 5 } + assert_equal 5, person.balance.amount + assert_kind_of Money, person.balance + end end class OverridingAggregationsTest < ActiveRecord::TestCase diff --git a/vendor/rails/activerecord/test/cases/associations/belongs_to_associations_test.rb b/vendor/rails/activerecord/test/cases/associations/belongs_to_associations_test.rb index 9c718c4..37b6836 100644 --- a/vendor/rails/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/vendor/rails/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -47,6 +47,19 @@ def test_natural_assignment assert_equal apple.id, citibank.firm_id end + def test_foreign_key_assignment + # Test using an existing record + signals37 = accounts(:signals37) + assert_equal companies(:first_firm), signals37.firm + signals37.firm_id = companies(:another_firm).id + assert_equal companies(:another_firm), signals37.firm + + # Test using a new record + account = Account.new + account.firm_id = companies(:another_firm).id + assert_equal companies(:another_firm), account.firm + end + def test_no_unexpected_aliasing first_firm = companies(:first_firm) another_firm = companies(:another_firm) diff --git a/vendor/rails/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/vendor/rails/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index edca3c6..9981f4c 100644 --- a/vendor/rails/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/vendor/rails/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -636,6 +636,18 @@ def test_join_with_group assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL', :group => group.join(",")).size end + def test_find_grouped + all_posts_from_category1 = Post.find(:all, :conditions => "category_id = 1", :joins => :categories) + grouped_posts_of_category1 = Post.find(:all, :conditions => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories) + assert_equal 4, all_posts_from_category1.size + assert_equal 1, grouped_posts_of_category1.size + end + + def test_find_scoped_grouped + assert_equal 4, categories(:general).posts_gruoped_by_title.size + assert_equal 1, categories(:technology).posts_gruoped_by_title.size + end + def test_get_ids assert_equal projects(:active_record, :action_controller).map(&:id).sort, developers(:david).project_ids.sort assert_equal [projects(:active_record).id], developers(:jamis).project_ids diff --git a/vendor/rails/activerecord/test/cases/associations/has_many_associations_test.rb b/vendor/rails/activerecord/test/cases/associations/has_many_associations_test.rb index feac4b0..ba750b2 100644 --- a/vendor/rails/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/vendor/rails/activerecord/test/cases/associations/has_many_associations_test.rb @@ -248,6 +248,13 @@ def test_find_grouped assert_equal 1, grouped_clients_of_firm1.size end + def test_find_scoped_grouped + assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.size + assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.length + assert_equal 2, companies(:first_firm).clients_grouped_by_name.size + assert_equal 2, companies(:first_firm).clients_grouped_by_name.length + end + def test_adding force_signal37_to_load_all_clients_of_firm natural = Client.new("name" => "Natural Company") diff --git a/vendor/rails/activerecord/test/cases/associations_test.rb b/vendor/rails/activerecord/test/cases/associations_test.rb index 0b2731e..2050d16 100644 --- a/vendor/rails/activerecord/test/cases/associations_test.rb +++ b/vendor/rails/activerecord/test/cases/associations_test.rb @@ -182,6 +182,18 @@ def test_failed_reset_returns_nil assert_nil p.author.reset end + def test_reset_loads_association_next_time + welcome = posts(:welcome) + david = authors(:david) + author_assoc = welcome.author + + assert_equal david, welcome.author # So we can be sure the test works correctly + author_assoc.reset + assert !author_assoc.loaded? + assert_nil author_assoc.target + assert_equal david, welcome.author + end + def test_reload_returns_assocition david = developers(:david) assert_nothing_raised do @@ -189,114 +201,6 @@ def test_reload_returns_assocition end end - def test_belongs_to_mass_assignment - post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } - author_attributes = { :name => 'David Dollar' } - - assert_no_difference 'Author.count' do - assert_raise(ActiveRecord::AssociationTypeMismatch) do - Post.create(post_attributes.merge({:author => author_attributes})) - end - end - - assert_difference 'Author.count' do - post = Post.create(post_attributes.merge({:creatable_author => author_attributes})) - assert_equal post.creatable_author.name, author_attributes[:name] - end - end - - def test_has_one_mass_assignment - post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } - comment_attributes = { :body => 'Setter Takes Hash' } - - assert_no_difference 'Comment.count' do - assert_raise(ActiveRecord::AssociationTypeMismatch) do - Post.create(post_attributes.merge({:uncreatable_comment => comment_attributes})) - end - end - - assert_difference 'Comment.count' do - post = Post.create(post_attributes.merge({:creatable_comment => comment_attributes})) - assert_equal post.creatable_comment.body, comment_attributes[:body] - end - end - - def test_has_many_mass_assignment - post = posts(:welcome) - post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } - comment_attributes = { :body => 'Setter Takes Hash' } - - assert_no_difference 'Comment.count' do - assert_raise(ActiveRecord::AssociationTypeMismatch) do - Post.create(post_attributes.merge({:comments => [comment_attributes]})) - end - assert_raise(ActiveRecord::AssociationTypeMismatch) do - post.comments << comment_attributes - end - end - - assert_difference 'Comment.count' do - post = Post.create(post_attributes.merge({:creatable_comments => [comment_attributes]})) - assert_equal post.creatable_comments.last.body, comment_attributes[:body] - end - - assert_difference 'Comment.count' do - post.creatable_comments << comment_attributes - assert_equal post.comments.last.body, comment_attributes[:body] - end - - post.creatable_comments = [comment_attributes, comment_attributes] - assert_equal post.creatable_comments.count, 2 - end - - def test_has_and_belongs_to_many_mass_assignment - post = posts(:welcome) - post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } - category_attributes = { :name => 'Accessible Association', :type => 'Category' } - - assert_no_difference 'Category.count' do - assert_raise(ActiveRecord::AssociationTypeMismatch) do - Post.create(post_attributes.merge({:categories => [category_attributes]})) - end - assert_raise(ActiveRecord::AssociationTypeMismatch) do - post.categories << category_attributes - end - end - - assert_difference 'Category.count' do - post = Post.create(post_attributes.merge({:creatable_categories => [category_attributes]})) - assert_equal post.creatable_categories.last.name, category_attributes[:name] - end - - assert_difference 'Category.count' do - post.creatable_categories << category_attributes - assert_equal post.creatable_categories.last.name, category_attributes[:name] - end - - post.creatable_categories = [category_attributes, category_attributes] - assert_equal post.creatable_categories.count, 2 - end - - def test_association_proxy_setter_can_take_hash - special_comment_attributes = { :body => 'Setter Takes Hash' } - - post = posts(:welcome) - post.creatable_comment = { :body => 'Setter Takes Hash' } - - assert_equal post.creatable_comment.body, special_comment_attributes[:body] - end - - def test_association_collection_can_take_hash - post_attributes = { :title => 'Setter Takes', :body => 'Hash' } - david = authors(:david) - - post = (david.posts << post_attributes).last - assert_equal post.title, post_attributes[:title] - - david.posts = [post_attributes, post_attributes] - assert_equal david.posts.count, 2 - end - def setup_dangling_association josh = Author.create(:name => "Josh") p = Post.create(:title => "New on Edge", :body => "More cool stuff!", :author => josh) diff --git a/vendor/rails/activerecord/test/cases/attribute_methods_test.rb b/vendor/rails/activerecord/test/cases/attribute_methods_test.rb index 7999e29..ce293a4 100644 --- a/vendor/rails/activerecord/test/cases/attribute_methods_test.rb +++ b/vendor/rails/activerecord/test/cases/attribute_methods_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/topic' +require 'models/minimalistic' class AttributeMethodsTest < ActiveRecord::TestCase fixtures :topics @@ -219,6 +220,14 @@ def test_setting_time_zone_aware_attribute_in_current_time_zone end end + def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable + Topic.skip_time_zone_conversion_for_attributes = [:field_a] + Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b] + + assert_equal [:field_a], Topic.skip_time_zone_conversion_for_attributes + assert_equal [:field_b], Minimalistic.skip_time_zone_conversion_for_attributes + end + private def time_related_columns_on_topic Topic.columns.select{|c| [:time, :date, :datetime, :timestamp].include?(c.type)}.map(&:name) diff --git a/vendor/rails/activerecord/test/cases/base_test.rb b/vendor/rails/activerecord/test/cases/base_test.rb index 67358fe..aebcca6 100644 --- a/vendor/rails/activerecord/test/cases/base_test.rb +++ b/vendor/rails/activerecord/test/cases/base_test.rb @@ -76,7 +76,7 @@ class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base end class BasicsTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts def test_table_exists assert !NonExistentTable.table_exists? @@ -664,10 +664,21 @@ def test_update_all_with_order_and_limit end end - def test_update_all_ignores_order_limit_from_association - author = Author.find(1) + def test_update_all_ignores_order_without_limit_from_association + author = authors(:david) assert_nothing_raised do - assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all("body = 'bulk update!'") + assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ]) + end + end + + def test_update_all_with_order_and_limit_updates_subset_only + author = authors(:david) + assert_nothing_raised do + assert_equal 1, author.posts_sorted_by_id_limited.size + assert_equal 2, author.posts_sorted_by_id_limited.find(:all, :limit => 2).size + assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ]) + assert_equal "bulk update!", posts(:welcome).body + assert_not_equal "bulk update!", posts(:thinking).body end end @@ -1073,6 +1084,24 @@ def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_at Time.zone = nil Topic.skip_time_zone_conversion_for_attributes = [] end + + def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion + ActiveRecord::Base.time_zone_aware_attributes = true + ActiveRecord::Base.default_timezone = :utc + Time.zone = ActiveSupport::TimeZone[-28800] + attributes = { + "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", + "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time + assert topic.bonus_time.utc? + ensure + ActiveRecord::Base.time_zone_aware_attributes = false + ActiveRecord::Base.default_timezone = :local + Time.zone = nil + end def test_multiparameter_attributes_on_time_with_empty_seconds attributes = { diff --git a/vendor/rails/activerecord/test/cases/defaults_test.rb b/vendor/rails/activerecord/test/cases/defaults_test.rb index 3473b84..ee84cb8 100644 --- a/vendor/rails/activerecord/test/cases/defaults_test.rb +++ b/vendor/rails/activerecord/test/cases/defaults_test.rb @@ -19,6 +19,37 @@ def test_nil_defaults_for_not_null_columns end if current_adapter?(:MysqlAdapter) + + #MySQL 5 and higher is quirky with not null text/blob columns. + #With MySQL Text/blob columns cannot have defaults. If the column is not null MySQL will report that the column has a null default + #but it behaves as though the column had a default of '' + def test_mysql_text_not_null_defaults + klass = Class.new(ActiveRecord::Base) + klass.table_name = 'test_mysql_text_not_null_defaults' + klass.connection.create_table klass.table_name do |t| + t.column :non_null_text, :text, :null => false + t.column :non_null_blob, :blob, :null => false + t.column :null_text, :text, :null => true + t.column :null_blob, :blob, :null => true + end + assert_equal '', klass.columns_hash['non_null_blob'].default + assert_equal '', klass.columns_hash['non_null_text'].default + + assert_equal nil, klass.columns_hash['null_blob'].default + assert_equal nil, klass.columns_hash['null_text'].default + + assert_nothing_raised do + instance = klass.create! + assert_equal '', instance.non_null_text + assert_equal '', instance.non_null_blob + assert_nil instance.null_text + assert_nil instance.null_blob + end + ensure + klass.connection.drop_table(klass.table_name) rescue nil + end + + # MySQL uses an implicit default 0 rather than NULL unless in strict mode. # We use an implicit NULL so schema.rb is compatible with other databases. def test_mysql_integer_not_null_defaults diff --git a/vendor/rails/activerecord/test/cases/dirty_test.rb b/vendor/rails/activerecord/test/cases/dirty_test.rb index 4fe1d79..39d38c4 100644 --- a/vendor/rails/activerecord/test/cases/dirty_test.rb +++ b/vendor/rails/activerecord/test/cases/dirty_test.rb @@ -45,6 +45,19 @@ def test_attribute_changes assert_nil pirate.catchphrase_change end + def test_aliased_attribute_changes + # the actual attribute here is name, title is an + # alias setup via alias_attribute + parrot = Parrot.new + assert !parrot.title_changed? + assert_nil parrot.title_change + + parrot.name = 'Sam' + assert parrot.title_changed? + assert_nil parrot.title_was + assert_equal parrot.name_change, parrot.title_change + end + def test_nullable_integer_not_marked_as_changed_if_new_value_is_blank pirate = Pirate.new diff --git a/vendor/rails/activerecord/test/cases/finder_test.rb b/vendor/rails/activerecord/test/cases/finder_test.rb index 2ce49ed..cbdff38 100644 --- a/vendor/rails/activerecord/test/cases/finder_test.rb +++ b/vendor/rails/activerecord/test/cases/finder_test.rb @@ -169,6 +169,12 @@ def test_find_with_limit_and_condition assert_equal("fixture_3", developers.first.name) end + def test_find_with_group + developers = Developer.find(:all, :group => "salary", :select => "salary") + assert_equal 4, developers.size + assert_equal 4, developers.map(&:salary).uniq.size + end + def test_find_with_entire_select_statement topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'" @@ -197,11 +203,11 @@ def test_find_first_failing first = Topic.find(:first, :conditions => "title = 'The First Topic!'") assert_nil(first) end - + def test_first assert_equal topics(:second).title, Topic.first(:conditions => "title = 'The Second Topic of the day'").title end - + def test_first_failing assert_nil Topic.first(:conditions => "title = 'The Second Topic of the day!'") end @@ -297,7 +303,6 @@ def test_find_on_multiple_hash_conditions assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) } end - def test_condition_interpolation assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"]) assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"]) @@ -392,7 +397,7 @@ def test_bind_variables Company.find(:first, :conditions => ["id=? AND name = ?", 2]) } assert_raises(ActiveRecord::PreparedStatementInvalid) { - Company.find(:first, :conditions => ["id=?", 2, 3, 4]) + Company.find(:first, :conditions => ["id=?", 2, 3, 4]) } end @@ -418,7 +423,7 @@ def test_bind_arity def test_named_bind_variables assert_equal '1', bind(':a', :a => 1) # ' ruby-mode assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode - + assert_nothing_raised { bind("'+00:00'", :foo => "bar") } assert_kind_of Firm, Company.find(:first, :conditions => ["name = :name", { :name => "37signals" }]) @@ -455,6 +460,15 @@ def test_bind_string assert_equal ActiveRecord::Base.connection.quote(''), bind('?', '') end + def test_bind_chars + quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") + quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi") + assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".chars) + assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".chars) + end + def test_bind_record o = Struct.new(:quoted_id).new(1) assert_equal '1', bind('?', o) @@ -589,6 +603,38 @@ def test_find_by_two_attributes assert_nil Topic.find_by_title_and_author_name("The First Topic", "Mary") end + def test_find_last_by_one_attribute + assert_equal Topic.last, Topic.find_last_by_title(Topic.last.title) + assert_nil Topic.find_last_by_title("A title with no matches") + end + + def test_find_last_by_one_attribute_caches_dynamic_finder + # ensure this test can run independently of order + class << Topic; self; end.send(:remove_method, :find_last_by_title) if Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' } + assert !Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' } + t = Topic.find_last_by_title(Topic.last.title) + assert Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' } + end + + def test_find_last_by_invalid_method_syntax + assert_raises(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") } + assert_raises(NoMethodError) { Topic.find_last_by_title?("The First Topic") } + end + + def test_find_last_by_one_attribute_with_several_options + assert_equal accounts(:signals37), Account.find_last_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3]) + end + + def test_find_last_by_one_missing_attribute + assert_raises(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") } + end + + def test_find_last_by_two_attributes + topic = Topic.last + assert_equal topic, Topic.find_last_by_title_and_author_name(topic.title, topic.author_name) + assert_nil Topic.find_last_by_title_and_author_name(topic.title, "Anonymous") + end + def test_find_all_by_one_attribute topics = Topic.find_all_by_content("Have a nice day") assert_equal 2, topics.size @@ -782,7 +828,7 @@ def test_find_or_create_should_set_protected_attributes_if_given_as_block assert c.valid? assert !c.new_record? end - + def test_dynamic_find_or_initialize_from_one_attribute_caches_method class << Company; self; end.send(:remove_method, :find_or_initialize_by_name) if Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' } assert !Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' } diff --git a/vendor/rails/activerecord/test/cases/helper.rb b/vendor/rails/activerecord/test/cases/helper.rb index f30d585..f7bdac8 100644 --- a/vendor/rails/activerecord/test/cases/helper.rb +++ b/vendor/rails/activerecord/test/cases/helper.rb @@ -46,3 +46,17 @@ def execute_with_query_record(sql, name = nil, &block) class << ActiveRecord::Base public :with_scope, :with_exclusive_scope end + +unless ENV['FIXTURE_DEBUG'] + module Test #:nodoc: + module Unit #:nodoc: + class << TestCase #:nodoc: + def try_to_load_dependency_with_silence(*args) + ActiveRecord::Base.logger.silence { try_to_load_dependency_without_silence(*args)} + end + + alias_method_chain :try_to_load_dependency, :silence + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/cases/inheritance_test.rb b/vendor/rails/activerecord/test/cases/inheritance_test.rb index 4fd38bf..a394156 100644 --- a/vendor/rails/activerecord/test/cases/inheritance_test.rb +++ b/vendor/rails/activerecord/test/cases/inheritance_test.rb @@ -193,7 +193,7 @@ def test_eager_load_belongs_to_something_inherited def test_eager_load_belongs_to_primary_key_quoting con = Account.connection - assert_sql(/\(#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)\)/) do + assert_sql(/\(#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1\)/) do Account.find(1, :include => :firm) end end diff --git a/vendor/rails/activerecord/test/cases/migration_test.rb b/vendor/rails/activerecord/test/cases/migration_test.rb index c1a8da2..ac44dd7 100644 --- a/vendor/rails/activerecord/test/cases/migration_test.rb +++ b/vendor/rails/activerecord/test/cases/migration_test.rb @@ -1133,7 +1133,11 @@ def test_create_table_with_binary_column columns = Person.connection.columns(:binary_testings) data_column = columns.detect { |c| c.name == "data" } - assert_nil data_column.default + if current_adapter?(:MysqlAdapter) + assert_equal '', data_column.default + else + assert_nil data_column.default + end Person.connection.drop_table :binary_testings rescue nil end diff --git a/vendor/rails/activerecord/test/cases/reflection_test.rb b/vendor/rails/activerecord/test/cases/reflection_test.rb index 4b86e32..e339ef4 100644 --- a/vendor/rails/activerecord/test/cases/reflection_test.rb +++ b/vendor/rails/activerecord/test/cases/reflection_test.rb @@ -160,8 +160,8 @@ def test_association_reflection_in_modules def test_reflection_of_all_associations # FIXME these assertions bust a lot - assert_equal 24, Firm.reflect_on_all_associations.size - assert_equal 18, Firm.reflect_on_all_associations(:has_many).size + assert_equal 26, Firm.reflect_on_all_associations.size + assert_equal 20, Firm.reflect_on_all_associations(:has_many).size assert_equal 6, Firm.reflect_on_all_associations(:has_one).size assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size end diff --git a/vendor/rails/activerecord/test/cases/sanitize_test.rb b/vendor/rails/activerecord/test/cases/sanitize_test.rb new file mode 100644 index 0000000..0106572 --- /dev/null +++ b/vendor/rails/activerecord/test/cases/sanitize_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" +require 'models/binary' + +class SanitizeTest < ActiveRecord::TestCase + def setup + end + + def test_sanitize_sql_array_handles_string_interpolation + quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi") + assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"]) + assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi".chars]) + quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper"]) + assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper".chars]) + end + + def test_sanitize_sql_array_handles_bind_variables + quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") + assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi"]) + assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi".chars]) + quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"]) + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".chars]) + end +end diff --git a/vendor/rails/activerecord/test/cases/validations_i18n_test.rb b/vendor/rails/activerecord/test/cases/validations_i18n_test.rb index 090f347..e34890e 100644 --- a/vendor/rails/activerecord/test/cases/validations_i18n_test.rb +++ b/vendor/rails/activerecord/test/cases/validations_i18n_test.rb @@ -40,6 +40,32 @@ def test_default_error_messages_is_deprecated end end + def test_percent_s_interpolation_syntax_in_error_messages_still_works + ActiveSupport::Deprecation.silence do + result = I18n.t :does_not_exist, :default => "%s interpolation syntax is deprecated", :value => 'this' + assert_equal result, "this interpolation syntax is deprecated" + end + end + + def test_percent_s_interpolation_syntax_in_error_messages_is_deprecated + assert_deprecated('using %s in messages') do + I18n.t :does_not_exist, :default => "%s interpolation syntax is deprected", :value => 'this' + end + end + + def test_percent_d_interpolation_syntax_in_error_messages_still_works + ActiveSupport::Deprecation.silence do + result = I18n.t :does_not_exist, :default => "%d interpolation syntaxes are deprecated", :count => 2 + assert_equal result, "2 interpolation syntaxes are deprecated" + end + end + + def test_percent_d_interpolation_syntax_in_error_messages_is_deprecated + assert_deprecated('using %d in messages') do + I18n.t :does_not_exist, :default => "%d interpolation syntaxes are deprected", :count => 2 + end + end + # ActiveRecord::Errors uses_mocha 'ActiveRecord::Errors' do diff --git a/vendor/rails/activerecord/test/cases/validations_test.rb b/vendor/rails/activerecord/test/cases/validations_test.rb index 4999d93..c049659 100644 --- a/vendor/rails/activerecord/test/cases/validations_test.rb +++ b/vendor/rails/activerecord/test/cases/validations_test.rb @@ -364,6 +364,13 @@ def test_validate_uniqueness assert t2.save, "Should now save t2 as unique" end + def test_validates_uniquness_with_newline_chars + Topic.validates_uniqueness_of(:title, :case_sensitive => false) + + t = Topic.new("title" => "new\nline") + assert t.save, "Should save t as unique" + end + def test_validate_uniqueness_with_scope Reply.validates_uniqueness_of(:content, :scope => "parent_id") @@ -605,7 +612,7 @@ def test_validate_format_numeric end def test_validate_format_with_formatted_message - Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be %s") + Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be {{value}}") t = Topic.create(:title => 'Invalid title') assert_equal "can't be Invalid title", t.errors.on(:title) end @@ -666,7 +673,7 @@ def test_validates_length_of_with_allow_blank end def test_validates_inclusion_of_with_formatted_message - Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :message => "option %s is not in the list" ) + Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :message => "option {{value}} is not in the list" ) assert Topic.create("title" => "a", "content" => "abc").valid? @@ -691,7 +698,7 @@ def test_validates_exclusion_of end def test_validates_exclusion_of_with_formatted_message - Topic.validates_exclusion_of( :title, :in => %w( abe monkey ), :message => "option %s is restricted" ) + Topic.validates_exclusion_of( :title, :in => %w( abe monkey ), :message => "option {{value}} is restricted" ) assert Topic.create("title" => "something", "content" => "abc") @@ -791,7 +798,7 @@ def test_optionally_validates_length_of_using_within end def test_optionally_validates_length_of_using_within_on_create - Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "my string is too long: %d" + Topic.validates_length_of :title, :content, :within => 5..10, :on => :create, :too_long => "my string is too long: {{count}}" t = Topic.create("title" => "thisisnotvalid", "content" => "whatever") assert !t.save @@ -812,7 +819,7 @@ def test_optionally_validates_length_of_using_within_on_create end def test_optionally_validates_length_of_using_within_on_update - Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "my string is too short: %d" + Topic.validates_length_of :title, :content, :within => 5..10, :on => :update, :too_short => "my string is too short: {{count}}" t = Topic.create("title" => "vali", "content" => "whatever") assert !t.save @@ -875,7 +882,7 @@ def test_validates_length_of_using_bignum def test_validates_length_with_globally_modified_error_message ActiveSupport::Deprecation.silence do - ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d' + ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre {{count}}' end Topic.validates_length_of :title, :minimum => 10 t = Topic.create(:title => 'too short') @@ -919,7 +926,7 @@ def test_validates_length_of_nasty_params end def test_validates_length_of_custom_errors_for_minimum_with_message - Topic.validates_length_of( :title, :minimum=>5, :message=>"boo %d" ) + Topic.validates_length_of( :title, :minimum=>5, :message=>"boo {{count}}" ) t = Topic.create("title" => "uhoh", "content" => "whatever") assert !t.valid? assert t.errors.on(:title) @@ -927,7 +934,7 @@ def test_validates_length_of_custom_errors_for_minimum_with_message end def test_validates_length_of_custom_errors_for_minimum_with_too_short - Topic.validates_length_of( :title, :minimum=>5, :too_short=>"hoo %d" ) + Topic.validates_length_of( :title, :minimum=>5, :too_short=>"hoo {{count}}" ) t = Topic.create("title" => "uhoh", "content" => "whatever") assert !t.valid? assert t.errors.on(:title) @@ -935,7 +942,7 @@ def test_validates_length_of_custom_errors_for_minimum_with_too_short end def test_validates_length_of_custom_errors_for_maximum_with_message - Topic.validates_length_of( :title, :maximum=>5, :message=>"boo %d" ) + Topic.validates_length_of( :title, :maximum=>5, :message=>"boo {{count}}" ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert !t.valid? assert t.errors.on(:title) @@ -943,7 +950,7 @@ def test_validates_length_of_custom_errors_for_maximum_with_message end def test_validates_length_of_custom_errors_for_maximum_with_too_long - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d" ) + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}" ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert !t.valid? assert t.errors.on(:title) @@ -951,7 +958,7 @@ def test_validates_length_of_custom_errors_for_maximum_with_too_long end def test_validates_length_of_custom_errors_for_is_with_message - Topic.validates_length_of( :title, :is=>5, :message=>"boo %d" ) + Topic.validates_length_of( :title, :is=>5, :message=>"boo {{count}}" ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert !t.valid? assert t.errors.on(:title) @@ -959,7 +966,7 @@ def test_validates_length_of_custom_errors_for_is_with_message end def test_validates_length_of_custom_errors_for_is_with_wrong_length - Topic.validates_length_of( :title, :is=>5, :wrong_length=>"hoo %d" ) + Topic.validates_length_of( :title, :is=>5, :wrong_length=>"hoo {{count}}" ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert !t.valid? assert t.errors.on(:title) @@ -1025,7 +1032,7 @@ def test_optionally_validates_length_of_using_within_utf8 def test_optionally_validates_length_of_using_within_on_create_utf8 with_kcode('UTF8') do - Topic.validates_length_of :title, :within => 5..10, :on => :create, :too_long => "長すぎます: %d" + Topic.validates_length_of :title, :within => 5..10, :on => :create, :too_long => "長すぎます: {{count}}" t = Topic.create("title" => "一二三四五六七八九十A", "content" => "whatever") assert !t.save @@ -1048,7 +1055,7 @@ def test_optionally_validates_length_of_using_within_on_create_utf8 def test_optionally_validates_length_of_using_within_on_update_utf8 with_kcode('UTF8') do - Topic.validates_length_of :title, :within => 5..10, :on => :update, :too_short => "短すぎます: %d" + Topic.validates_length_of :title, :within => 5..10, :on => :update, :too_short => "短すぎます: {{count}}" t = Topic.create("title" => "一二三4", "content" => "whatever") assert !t.save @@ -1083,7 +1090,7 @@ def test_validates_length_of_using_is_utf8 end def test_validates_length_of_with_block - Topic.validates_length_of :content, :minimum => 5, :too_short=>"Your essay must be at least %d words.", + Topic.validates_length_of :content, :minimum => 5, :too_short=>"Your essay must be at least {{count}} words.", :tokenizer => lambda {|str| str.scan(/\w+/) } t = Topic.create!(:content => "this content should be long enough") assert t.valid? @@ -1234,7 +1241,7 @@ def test_validates_associated_with_custom_message_using_quotes def test_if_validation_using_method_true # When the method returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => :condition_is_true ) + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => :condition_is_true ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert !t.valid? assert t.errors.on(:title) @@ -1243,7 +1250,7 @@ def test_if_validation_using_method_true def test_unless_validation_using_method_true # When the method returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => :condition_is_true ) + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert !t.errors.on(:title) @@ -1251,7 +1258,7 @@ def test_unless_validation_using_method_true def test_if_validation_using_method_false # When the method returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => :condition_is_true_but_its_not ) + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => :condition_is_true_but_its_not ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert !t.errors.on(:title) @@ -1259,7 +1266,7 @@ def test_if_validation_using_method_false def test_unless_validation_using_method_false # When the method returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => :condition_is_true_but_its_not ) + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => :condition_is_true_but_its_not ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert !t.valid? assert t.errors.on(:title) @@ -1268,7 +1275,7 @@ def test_unless_validation_using_method_false def test_if_validation_using_string_true # When the evaluated string returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => "a = 1; a == 1" ) + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => "a = 1; a == 1" ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert !t.valid? assert t.errors.on(:title) @@ -1277,7 +1284,7 @@ def test_if_validation_using_string_true def test_unless_validation_using_string_true # When the evaluated string returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => "a = 1; a == 1" ) + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "a = 1; a == 1" ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert !t.errors.on(:title) @@ -1285,7 +1292,7 @@ def test_unless_validation_using_string_true def test_if_validation_using_string_false # When the evaluated string returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :if => "false") + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => "false") t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert !t.errors.on(:title) @@ -1293,7 +1300,7 @@ def test_if_validation_using_string_false def test_unless_validation_using_string_false # When the evaluated string returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", :unless => "false") + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => "false") t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert !t.valid? assert t.errors.on(:title) @@ -1302,7 +1309,7 @@ def test_unless_validation_using_string_false def test_if_validation_using_block_true # When the block returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => Proc.new { |r| r.content.size > 4 } ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert !t.valid? @@ -1312,7 +1319,7 @@ def test_if_validation_using_block_true def test_unless_validation_using_block_true # When the block returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => Proc.new { |r| r.content.size > 4 } ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert t.valid? @@ -1321,7 +1328,7 @@ def test_unless_validation_using_block_true def test_if_validation_using_block_false # When the block returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :if => Proc.new { |r| r.title != "uhohuhoh"} ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert t.valid? @@ -1330,7 +1337,7 @@ def test_if_validation_using_block_false def test_unless_validation_using_block_false # When the block returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %d", + Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}", :unless => Proc.new { |r| r.title != "uhohuhoh"} ) t = Topic.create("title" => "uhohuhoh", "content" => "whatever") assert !t.valid? @@ -1507,13 +1514,13 @@ def test_validates_numericality_with_greater_than_less_than_and_even end def test_validates_numericality_with_numeric_message - Topic.validates_numericality_of :approved, :less_than => 4, :message => "smaller than %d" + Topic.validates_numericality_of :approved, :less_than => 4, :message => "smaller than {{count}}" topic = Topic.new("title" => "numeric test", "approved" => 10) assert !topic.valid? assert_equal "smaller than 4", topic.errors.on(:approved) - Topic.validates_numericality_of :approved, :greater_than => 4, :message => "greater than %d" + Topic.validates_numericality_of :approved, :greater_than => 4, :message => "greater than {{count}}" topic = Topic.new("title" => "numeric test", "approved" => 1) assert !topic.valid? diff --git a/vendor/rails/activerecord/test/fixtures/customers.yml b/vendor/rails/activerecord/test/fixtures/customers.yml index f802aac..0399ff8 100644 --- a/vendor/rails/activerecord/test/fixtures/customers.yml +++ b/vendor/rails/activerecord/test/fixtures/customers.yml @@ -6,7 +6,7 @@ david: address_city: Scary Town address_country: Loony Land gps_location: 35.544623640962634x-105.9309951055148 - + zaphod: id: 2 name: Zaphod @@ -14,4 +14,13 @@ zaphod: address_street: Avenue Road address_city: Hamlet Town address_country: Nation Land + gps_location: NULL + +barney: + id: 3 + name: Barney Gumble + balance: 1 + address_street: Quiet Road + address_city: Peaceful Town + address_country: Tranquil Land gps_location: NULL \ No newline at end of file diff --git a/vendor/rails/activerecord/test/models/author.rb b/vendor/rails/activerecord/test/models/author.rb index c6aa029..37551c8 100644 --- a/vendor/rails/activerecord/test/models/author.rb +++ b/vendor/rails/activerecord/test/models/author.rb @@ -1,7 +1,8 @@ class Author < ActiveRecord::Base - has_many :posts, :accessible => true + has_many :posts has_many :posts_with_comments, :include => :comments, :class_name => "Post" has_many :posts_with_comments_sorted_by_comment_id, :include => :comments, :class_name => "Post", :order => 'comments.id' + has_many :posts_sorted_by_id_limited, :class_name => "Post", :order => 'posts.id', :limit => 1 has_many :posts_with_categories, :include => :categories, :class_name => "Post" has_many :posts_with_comments_and_categories, :include => [ :comments, :categories ], :order => "posts.id", :class_name => "Post" has_many :posts_containing_the_letter_a, :class_name => "Post" diff --git a/vendor/rails/activerecord/test/models/category.rb b/vendor/rails/activerecord/test/models/category.rb index 1660c61..4e9d247 100644 --- a/vendor/rails/activerecord/test/models/category.rb +++ b/vendor/rails/activerecord/test/models/category.rb @@ -13,6 +13,9 @@ class Category < ActiveRecord::Base has_and_belongs_to_many :post_with_conditions, :class_name => 'Post', :conditions => { :title => 'Yet Another Testing Title' } + + has_and_belongs_to_many :posts_gruoped_by_title, :class_name => "Post", :group => "title", :select => "title" + def self.what_are_you 'a category...' end diff --git a/vendor/rails/activerecord/test/models/company.rb b/vendor/rails/activerecord/test/models/company.rb index cd43594..0eb8ae0 100644 --- a/vendor/rails/activerecord/test/models/company.rb +++ b/vendor/rails/activerecord/test/models/company.rb @@ -55,6 +55,8 @@ class Firm < Company has_many :readonly_clients, :class_name => 'Client', :readonly => true has_many :clients_using_primary_key, :class_name => 'Client', :primary_key => 'name', :foreign_key => 'firm_name' + has_many :clients_grouped_by_firm_id, :class_name => "Client", :group => "firm_id", :select => "firm_id" + has_many :clients_grouped_by_name, :class_name => "Client", :group => "name", :select => "name" has_one :account, :foreign_key => "firm_id", :dependent => :destroy, :validate => true has_one :unvalidated_account, :foreign_key => "firm_id", :class_name => 'Account', :validate => false diff --git a/vendor/rails/activerecord/test/models/customer.rb b/vendor/rails/activerecord/test/models/customer.rb index 030bbc6..e258ccd 100644 --- a/vendor/rails/activerecord/test/models/customer.rb +++ b/vendor/rails/activerecord/test/models/customer.rb @@ -1,7 +1,8 @@ class Customer < ActiveRecord::Base composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true - composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money } + composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money } composed_of :gps_location, :allow_nil => true + composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse end class Address @@ -53,3 +54,20 @@ def ==(other) self.latitude == other.latitude && self.longitude == other.longitude end end + +class Fullname + attr_reader :first, :last + + def self.parse(str) + return nil unless str + new(*str.to_s.split) + end + + def initialize(first, last = nil) + @first, @last = first, last + end + + def to_s + "#{first} #{last.upcase}" + end +end \ No newline at end of file diff --git a/vendor/rails/activerecord/test/models/parrot.rb b/vendor/rails/activerecord/test/models/parrot.rb index 65191c1..b9431fd 100644 --- a/vendor/rails/activerecord/test/models/parrot.rb +++ b/vendor/rails/activerecord/test/models/parrot.rb @@ -3,6 +3,7 @@ class Parrot < ActiveRecord::Base has_and_belongs_to_many :pirates has_and_belongs_to_many :treasures has_many :loots, :as => :looter + alias_attribute :title, :name end class LiveParrot < Parrot diff --git a/vendor/rails/activerecord/test/models/post.rb b/vendor/rails/activerecord/test/models/post.rb index e23818e..3adbc0c 100644 --- a/vendor/rails/activerecord/test/models/post.rb +++ b/vendor/rails/activerecord/test/models/post.rb @@ -33,12 +33,6 @@ def find_most_recent has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' - belongs_to :creatable_author, :class_name => 'Author', :accessible => true - has_one :uncreatable_comment, :class_name => 'Comment', :accessible => false, :order => 'id desc' - has_one :creatable_comment, :class_name => 'Comment', :accessible => true, :order => 'id desc' - has_many :creatable_comments, :class_name => 'Comment', :accessible => true, :dependent => :destroy - has_and_belongs_to_many :creatable_categories, :class_name => 'Category', :accessible => true - has_many :taggings, :as => :taggable has_many :tags, :through => :taggings do def add_joins_and_select diff --git a/vendor/rails/activesupport/CHANGELOG b/vendor/rails/activesupport/CHANGELOG index 0170b95..00da2a2 100644 --- a/vendor/rails/activesupport/CHANGELOG +++ b/vendor/rails/activesupport/CHANGELOG @@ -1,5 +1,15 @@ *Edge* +* TimeWithZone #wday, #yday and #to_date avoid trip through #method_missing [Geoff Buesing] + +* Added Time, Date, DateTime and TimeWithZone #past?, #future? and #today? #720 [Clemens Kofler, Geoff Buesing] + +* Fixed Sri Jayawardenepura time zone to map to Asia/Colombo [Jamis Buck] + +* Added Inflector#parameterize for easy slug generation ("Donald E. Knuth".parameterize => "donald-e-knuth") #713 [Matt Darby] + +* Changed cache benchmarking to be reported in milliseconds [DHH] + * Fix Ruby's Time marshaling bug in pre-1.9 versions of Ruby: utc instances are now correctly unmarshaled with a utc zone instead of the system local zone [#900 state:resolved] [Luca Guidi, Geoff Buesing] * Add Array#in_groups which splits or iterates over the array in specified number of groups. #579. [Adrian Mugnolo] Example: diff --git a/vendor/rails/activesupport/lib/active_support/cache.rb b/vendor/rails/activesupport/lib/active_support/cache.rb index 95eae3a..51a6309 100644 --- a/vendor/rails/activesupport/lib/active_support/cache.rb +++ b/vendor/rails/activesupport/lib/active_support/cache.rb @@ -62,7 +62,7 @@ def fetch(key, options = {}) write(key, value, options) @logger_off = false - log("write (will save #{'%.5f' % seconds})", key, nil) + log("write (will save #{'%.2f' % (seconds * 1000)}ms)", key, nil) value end diff --git a/vendor/rails/activesupport/lib/active_support/cache/file_store.rb b/vendor/rails/activesupport/lib/active_support/cache/file_store.rb index 659bde6..ef53363 100644 --- a/vendor/rails/activesupport/lib/active_support/cache/file_store.rb +++ b/vendor/rails/activesupport/lib/active_support/cache/file_store.rb @@ -65,6 +65,6 @@ def search_dir(dir, &callback) end end end - end + end end end diff --git a/vendor/rails/activesupport/lib/active_support/cache/memory_store.rb b/vendor/rails/activesupport/lib/active_support/cache/memory_store.rb index f3e4b8c..c1a713b 100644 --- a/vendor/rails/activesupport/lib/active_support/cache/memory_store.rb +++ b/vendor/rails/activesupport/lib/active_support/cache/memory_store.rb @@ -3,63 +3,35 @@ module Cache class MemoryStore < Store def initialize @data = {} - @guard = Monitor.new - end - - def fetch(key, options = {}) - @guard.synchronize do - super - end end def read(name, options = nil) - @guard.synchronize do - super - @data[name] - end + super + @data[name] end def write(name, value, options = nil) - @guard.synchronize do - super - @data[name] = value.freeze - end + super + @data[name] = value.freeze end def delete(name, options = nil) - @guard.synchronize do - @data.delete(name) - end + super + @data.delete(name) end def delete_matched(matcher, options = nil) - @guard.synchronize do - @data.delete_if { |k,v| k =~ matcher } - end + super + @data.delete_if { |k,v| k =~ matcher } end def exist?(name,options = nil) - @guard.synchronize do - @data.has_key?(name) - end - end - - def increment(key, amount = 1) - @guard.synchronize do - super - end - end - - def decrement(key, amount = 1) - @guard.synchronize do - super - end + super + @data.has_key?(name) end def clear - @guard.synchronize do - @data.clear - end + @data.clear end end end diff --git a/vendor/rails/activesupport/lib/active_support/cache/synchronized_memory_store.rb b/vendor/rails/activesupport/lib/active_support/cache/synchronized_memory_store.rb new file mode 100644 index 0000000..d2ff287 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/cache/synchronized_memory_store.rb @@ -0,0 +1,46 @@ +module ActiveSupport + module Cache + class SynchronizedMemoryStore < MemoryStore + def initialize + super + @guard = Monitor.new + end + + def fetch(key, options = {}) + @guard.synchronize { super } + end + + def read(name, options = nil) + @guard.synchronize { super } + end + + def write(name, value, options = nil) + @guard.synchronize { super } + end + + def delete(name, options = nil) + @guard.synchronize { super } + end + + def delete_matched(matcher, options = nil) + @guard.synchronize { super } + end + + def exist?(name,options = nil) + @guard.synchronize { super } + end + + def increment(key, amount = 1) + @guard.synchronize { super } + end + + def decrement(key, amount = 1) + @guard.synchronize { super } + end + + def clear + @guard.synchronize { super } + end + end + end +end diff --git a/vendor/rails/activesupport/lib/active_support/callbacks.rb b/vendor/rails/activesupport/lib/active_support/callbacks.rb index 7b90593..5cdcaf5 100644 --- a/vendor/rails/activesupport/lib/active_support/callbacks.rb +++ b/vendor/rails/activesupport/lib/active_support/callbacks.rb @@ -188,7 +188,7 @@ def evaluate_method(method, *args, &block) "Callbacks must be a symbol denoting the method to call, a string to be evaluated, " + "a block to be invoked, or an object responding to the callback method." end - end + end end def should_run_callback?(*args) diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/array/grouping.rb b/vendor/rails/activesupport/lib/active_support/core_ext/array/grouping.rb index dd1484f..f782f8f 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -7,16 +7,16 @@ module Grouping # Splits or iterates over the array in groups of size +number+, # padding any remaining slots with +fill_with+ unless it is +false+. # - # %w(1 2 3 4 5 6 7).in_groups_of(3) {|g| p g} + # %w(1 2 3 4 5 6 7).in_groups_of(3) {|group| p group} # ["1", "2", "3"] # ["4", "5", "6"] # ["7", nil, nil] # - # %w(1 2 3).in_groups_of(2, ' ') {|g| p g} + # %w(1 2 3).in_groups_of(2, ' ') {|group| p group} # ["1", "2"] # ["3", " "] # - # %w(1 2 3).in_groups_of(2, false) {|g| p g} + # %w(1 2 3).in_groups_of(2, false) {|group| p group} # ["1", "2"] # ["3"] def in_groups_of(number, fill_with = nil) @@ -42,17 +42,17 @@ def in_groups_of(number, fill_with = nil) # Splits or iterates over the array in +number+ of groups, padding any # remaining slots with +fill_with+ unless it is +false+. # - # %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|g| p g} + # %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group} # ["1", "2", "3", "4"] # ["5", "6", "7", nil] # ["8", "9", "10", nil] # - # %w(1 2 3 4 5 6 7).in_groups(3, ' ') {|g| p g} + # %w(1 2 3 4 5 6 7).in_groups(3, ' ') {|group| p group} # ["1", "2", "3"] # ["4", "5", " "] # ["6", "7", " "] # - # %w(1 2 3 4 5 6 7).in_groups(3, false) {|g| p g} + # %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group} # ["1", "2", "3"] # ["4", "5"] # ["6", "7"] diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/bigdecimal/conversions.rb b/vendor/rails/activesupport/lib/active_support/core_ext/bigdecimal/conversions.rb index 94c7c77..bc9d578 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/bigdecimal/conversions.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/bigdecimal/conversions.rb @@ -4,35 +4,31 @@ module ActiveSupport #:nodoc: module CoreExtensions #:nodoc: module BigDecimal #:nodoc: module Conversions + DEFAULT_STRING_FORMAT = 'F'.freeze + YAML_TAG = 'tag:yaml.org,2002:float'.freeze + YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' } + def self.included(base) #:nodoc: - base.instance_eval do + base.class_eval do alias_method :_original_to_s, :to_s alias_method :to_s, :to_formatted_s + + yaml_as YAML_TAG end end - - def to_formatted_s(format="F") + + def to_formatted_s(format = DEFAULT_STRING_FORMAT) _original_to_s(format) end - - yaml_as "tag:yaml.org,2002:float" - def to_yaml( opts = {} ) - YAML::quick_emit( nil, opts ) do |out| - # This emits the number without any scientific notation. - # I prefer it to using self.to_f.to_s, which would lose precision. - # - # Note that YAML allows that when reconstituting floats - # to native types, some precision may get lost. - # There is no full precision real YAML tag that I am aware of. - str = self.to_s - if str == "Infinity" - str = ".Inf" - elsif str == "-Infinity" - str = "-.Inf" - elsif str == "NaN" - str = ".NaN" - end - out.scalar( "tag:yaml.org,2002:float", str, :plain ) + + # This emits the number without any scientific notation. + # This is better than self.to_f.to_s since it doesn't lose precision. + # + # Note that reconstituting YAML floats to native floats may lose precision. + def to_yaml(opts = {}) + YAML.quick_emit(nil, opts) do |out| + string = to_s + out.scalar(YAML_TAG, YAML_MAPPING[string] || string, :plain) end end end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/date/calculations.rb b/vendor/rails/activesupport/lib/active_support/core_ext/date/calculations.rb index b5180c9..43d70c7 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -20,18 +20,33 @@ module ClassMethods def yesterday ::Date.today.yesterday end - + # Returns a new Date representing the date 1 day after today (i.e. tomorrow's date). def tomorrow ::Date.today.tomorrow end - + # Returns Time.zone.today when config.time_zone is set, otherwise just returns Date.today. def current ::Time.zone_default ? ::Time.zone.today : ::Date.today end end - + + # Tells whether the Date object's date lies in the past + def past? + self < ::Date.current + end + + # Tells whether the Date object's date is today + def today? + self.to_date == ::Date.current # we need the to_date because of DateTime + end + + # Tells whether the Date object's date lies in the future + def future? + self > ::Date.current + end + # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00) # and then subtracts the specified number of seconds def ago(seconds) @@ -57,7 +72,7 @@ def beginning_of_day def end_of_day to_time.end_of_day end - + def plus_with_duration(other) #:nodoc: if ActiveSupport::Duration === other other.since(self) @@ -65,7 +80,7 @@ def plus_with_duration(other) #:nodoc: plus_without_duration(other) end end - + def minus_with_duration(other) #:nodoc: if ActiveSupport::Duration === other plus_with_duration(-other) @@ -73,8 +88,8 @@ def minus_with_duration(other) #:nodoc: minus_without_duration(other) end end - - # Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with + + # Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with # any of these keys: :years, :months, :weeks, :days. def advance(options) d = self @@ -98,7 +113,7 @@ def change(options) options[:day] || self.day ) end - + # Returns a new Date/DateTime representing the time a number of specified months ago def months_ago(months) advance(:months => -months) @@ -161,7 +176,7 @@ def next_week(day = :monday) days_into_week = { :monday => 0, :tuesday => 1, :wednesday => 2, :thursday => 3, :friday => 4, :saturday => 5, :sunday => 6} result = (self + 7).beginning_of_week + days_into_week[day] self.acts_like?(:time) ? result.change(:hour => 0) : result - end + end # Returns a new ; DateTime objects will have time set to 0:00DateTime representing the start of the month (1st of the month; DateTime objects will have time set to 0:00) def beginning_of_month diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/vendor/rails/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 155c961..0099431 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -7,7 +7,7 @@ module DateTime #:nodoc: module Calculations def self.included(base) #:nodoc: base.extend ClassMethods - + base.class_eval do alias_method :compare_without_coercion, :<=> alias_method :<=>, :compare_with_coercion @@ -19,6 +19,20 @@ module ClassMethods def local_offset ::Time.local(2007).utc_offset.to_r / 86400 end + + def current + ::Time.zone_default ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime + end + end + + # Tells whether the DateTime object's datetime lies in the past + def past? + self < ::DateTime.current + end + + # Tells whether the DateTime object's datetime lies in the future + def future? + self > ::DateTime.current end # Seconds since midnight: DateTime.now.seconds_since_midnight @@ -78,7 +92,7 @@ def beginning_of_day def end_of_day change(:hour => 23, :min => 59, :sec => 59) end - + # Adjusts DateTime to UTC by adding its offset value; offset is set to 0 # # Example: @@ -89,17 +103,17 @@ def utc new_offset(0) end alias_method :getutc, :utc - + # Returns true if offset == 0 def utc? offset == 0 end - + # Returns the offset value in seconds def utc_offset (offset * 86400).to_i end - + # Layers additional behavior on DateTime#<=> so that Time and ActiveSupport::TimeWithZone instances can be compared with a DateTime def compare_with_coercion(other) other = other.comparable_time if other.respond_to?(:comparable_time) diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/enumerable.rb b/vendor/rails/activesupport/lib/active_support/core_ext/enumerable.rb index fd94bc0..788f3a7 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/enumerable.rb @@ -77,9 +77,9 @@ def sum(identity = 0, &block) # (1..5).each_with_object(1) { |value, memo| memo *= value } # => 1 # def each_with_object(memo, &block) - returning memo do |memo| + returning memo do |m| each do |element| - block.call(element, memo) + block.call(element, m) end end end unless [].respond_to?(:each_with_object) diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/file/atomic.rb b/vendor/rails/activesupport/lib/active_support/core_ext/file/atomic.rb index 4d3cf54..f988eff 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -28,7 +28,7 @@ def atomic_write(file_name, temp_dir = Dir.tmpdir) rescue Errno::ENOENT # No old permissions, write a temp file to determine the defaults check_name = ".permissions_check.#{Thread.current.object_id}.#{Process.pid}.#{rand(1000000)}" - new(check_name, "w") + open(check_name, "w") { } old_stat = stat(check_name) unlink(check_name) end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/hash/conversions.rb b/vendor/rails/activesupport/lib/active_support/core_ext/hash/conversions.rb index 2c606b4..50dc7c6 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -156,7 +156,7 @@ def to_xml(options = {}) XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value, attributes ) - end + end end end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/hash/except.rb b/vendor/rails/activesupport/lib/active_support/core_ext/hash/except.rb index f26d015..949976d 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/hash/except.rb @@ -10,7 +10,7 @@ module Hash #:nodoc: module Except # Returns a new hash without the given keys. def except(*keys) - clone.except!(*keys) + dup.except!(*keys) end # Replaces the hash without the given keys. diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/module/synchronization.rb b/vendor/rails/activesupport/lib/active_support/core_ext/module/synchronization.rb index 6253594..2516060 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/module/synchronization.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/module/synchronization.rb @@ -18,11 +18,13 @@ def synchronize(*methods) raise ArgumentError, "Synchronization needs a mutex. Supply an options hash with a :with key as the last argument (e.g. synchronize :hello, :with => :@mutex)." end - methods.flatten.each do |method| + methods.each do |method| aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1 - if instance_methods.include?("#{aliased_method}_without_synchronization#{punctuation}") + + if method_defined?("#{aliased_method}_without_synchronization#{punctuation}") raise ArgumentError, "#{method} is already synchronized. Double synchronization is not currently supported." end + module_eval(<<-EOS, __FILE__, __LINE__) def #{aliased_method}_with_synchronization#{punctuation}(*args, &block) #{with}.synchronize do @@ -30,7 +32,8 @@ def #{aliased_method}_with_synchronization#{punctuation}(*args, &block) end end EOS + alias_method_chain method, :synchronization end end -end \ No newline at end of file +end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/rexml.rb b/vendor/rails/activesupport/lib/active_support/core_ext/rexml.rb index af8ce3a..187f3e0 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/rexml.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/rexml.rb @@ -5,7 +5,8 @@ # http://www.ruby-lang.org/en/news/2008/08/23/dos-vulnerability-in-rexml/ # This fix is identical to rexml-expansion-fix version 1.0.1 -unless REXML::VERSION > "3.1.7.2" +# Earlier versions of rexml defined REXML::Version, newer ones REXML::VERSION +unless (defined?(REXML::VERSION) ? REXML::VERSION : REXML::Version) > "3.1.7.2" module REXML class Entity < Child undef_method :unnormalized diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/string.rb b/vendor/rails/activesupport/lib/active_support/core_ext/string.rb index 25386af..7ff2f11 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/string.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/string.rb @@ -6,6 +6,7 @@ require 'active_support/core_ext/string/unicode' require 'active_support/core_ext/string/xchar' require 'active_support/core_ext/string/filters' +require 'active_support/core_ext/string/behavior' class String #:nodoc: include ActiveSupport::CoreExtensions::String::Access @@ -15,4 +16,5 @@ class String #:nodoc: include ActiveSupport::CoreExtensions::String::StartsEndsWith include ActiveSupport::CoreExtensions::String::Iterators include ActiveSupport::CoreExtensions::String::Unicode + include ActiveSupport::CoreExtensions::String::Behavior end diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/string/behavior.rb b/vendor/rails/activesupport/lib/active_support/core_ext/string/behavior.rb new file mode 100644 index 0000000..a93ca30 --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/core_ext/string/behavior.rb @@ -0,0 +1,13 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module String #:nodoc: + module Behavior + # Enable more predictable duck-typing on String-like classes. See + # Object#acts_like?. + def acts_like_string? + true + end + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/string/inflections.rb b/vendor/rails/activesupport/lib/active_support/core_ext/string/inflections.rb index 3bbad7d..de99fe5 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -87,6 +87,25 @@ def demodulize Inflector.demodulize(self) end + # Replaces special characters in a string so that it may be used as part of a 'pretty' URL. + # + # ==== Examples + # + # class Person + # def to_param + # "#{id}-#{name.parameterize}" + # end + # end + # + # @person = Person.find(1) + # # => # + # + # <%= link_to(@person.name, person_path %> + # # => Donald E. Knuth + def parameterize + Inflector.parameterize(self) + end + # Creates the name of a table like Rails does for models to table names. This method # uses the +pluralize+ method on the last word in the string. # diff --git a/vendor/rails/activesupport/lib/active_support/core_ext/time/calculations.rb b/vendor/rails/activesupport/lib/active_support/core_ext/time/calculations.rb index cd234c9..070f72c 100644 --- a/vendor/rails/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/vendor/rails/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -9,13 +9,13 @@ def self.included(base) #:nodoc: base.class_eval do alias_method :plus_without_duration, :+ alias_method :+, :plus_with_duration - + alias_method :minus_without_duration, :- alias_method :-, :minus_with_duration - + alias_method :minus_without_coercion, :- alias_method :-, :minus_with_coercion - + alias_method :compare_without_coercion, :<=> alias_method :<=>, :compare_with_coercion end @@ -28,9 +28,9 @@ module ClassMethods def ===(other) other.is_a?(::Time) end - - # Return the number of days in the given month. - # If no year is specified, it will use the current year. + + # Return the number of days in the given month. + # If no year is specified, it will use the current year. def days_in_month(month, year = now.year) return 29 if month == 2 && ::Date.gregorian_leap?(year) COMMON_YEAR_DAYS_IN_MONTH[month] @@ -57,6 +57,21 @@ def local_time(*args) end end + # Tells whether the Time object's time lies in the past + def past? + self < ::Time.current + end + + # Tells whether the Time object's time is today + def today? + self.to_date == ::Date.current + end + + # Tells whether the Time object's time lies in the future + def future? + self > ::Time.current + end + # Seconds since midnight: Time.now.seconds_since_midnight def seconds_since_midnight self.to_i - self.change(:hour => 0).to_i + (self.usec/1.0e+6) @@ -106,7 +121,7 @@ def since(seconds) (seconds.abs >= 86400 && initial_dst != final_dst) ? f + (initial_dst - final_dst).hours : f end rescue - self.to_datetime.since(seconds) + self.to_datetime.since(seconds) end alias :in :since @@ -199,7 +214,7 @@ def end_of_month change(:day => last_day, :hour => 23, :min => 59, :sec => 59, :usec => 0) end alias :at_end_of_month :end_of_month - + # Returns a new Time representing the start of the quarter (1st of january, april, july, october, 0:00) def beginning_of_quarter beginning_of_month.change(:month => [10, 7, 4, 1].detect { |m| m <= self.month }) @@ -249,7 +264,7 @@ def minus_with_duration(other) #:nodoc: minus_without_duration(other) end end - + # Time#- can also be used to determine the number of seconds between two Time instances. # We're layering on additional behavior so that ActiveSupport::TimeWithZone instances # are coerced into values that Time#- will recognize @@ -257,7 +272,7 @@ def minus_with_coercion(other) other = other.comparable_time if other.respond_to?(:comparable_time) minus_without_coercion(other) end - + # Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances # can be chronologically compared with a Time def compare_with_coercion(other) diff --git a/vendor/rails/activesupport/lib/active_support/inflector.rb b/vendor/rails/activesupport/lib/active_support/inflector.rb index 7ae9e0c..8a917a9 100644 --- a/vendor/rails/activesupport/lib/active_support/inflector.rb +++ b/vendor/rails/activesupport/lib/active_support/inflector.rb @@ -240,6 +240,25 @@ def humanize(lower_case_and_underscored_word) def demodulize(class_name_in_module) class_name_in_module.to_s.gsub(/^.*::/, '') end + + # Replaces special characters in a string so that it may be used as part of a 'pretty' URL. + # + # ==== Examples + # + # class Person + # def to_param + # "#{id}-#{name.parameterize}" + # end + # end + # + # @person = Person.find(1) + # # => # + # + # <%= link_to(@person.name, person_path %> + # # => Donald E. Knuth + def parameterize(string, sep = '-') + string.chars.normalize(:kd).to_s.gsub(/[^\x00-\x7F]+/, '').gsub(/[^a-z0-9_\-]+/i, sep).downcase + end # Create the name of a table like Rails does for models to table names. This method # uses the +pluralize+ method on the last word in the string. diff --git a/vendor/rails/activesupport/lib/active_support/memoizable.rb b/vendor/rails/activesupport/lib/active_support/memoizable.rb index 6506238..4786fd6 100644 --- a/vendor/rails/activesupport/lib/active_support/memoizable.rb +++ b/vendor/rails/activesupport/lib/active_support/memoizable.rb @@ -60,7 +60,7 @@ def #{symbol}(*args) #{memoized_ivar} ||= {} unless frozen? reload = args.pop if args.last == true || args.last == :reload - if #{memoized_ivar} + if defined?(#{memoized_ivar}) && #{memoized_ivar} if !reload && #{memoized_ivar}.has_key?(args) #{memoized_ivar}[args] elsif #{memoized_ivar} diff --git a/vendor/rails/activesupport/lib/active_support/multibyte/chars.rb b/vendor/rails/activesupport/lib/active_support/multibyte/chars.rb index 185d030..de2c83f 100644 --- a/vendor/rails/activesupport/lib/active_support/multibyte/chars.rb +++ b/vendor/rails/activesupport/lib/active_support/multibyte/chars.rb @@ -49,6 +49,11 @@ def respond_to?(method, include_priv = false) false end + # Enable more predictable duck-typing on String-like classes. See Object#acts_like?. + def acts_like_string? + true + end + # Create a new Chars instance. def initialize(str) @string = str.respond_to?(:string) ? str.string : str diff --git a/vendor/rails/activesupport/lib/active_support/test_case.rb b/vendor/rails/activesupport/lib/active_support/test_case.rb index 0f531b0..197e73b 100644 --- a/vendor/rails/activesupport/lib/active_support/test_case.rb +++ b/vendor/rails/activesupport/lib/active_support/test_case.rb @@ -12,7 +12,13 @@ def self.test(name, &block) test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym defined = instance_method(test_name) rescue false raise "#{test_name} is already defined in #{self}" if defined - define_method(test_name, &block) + if block_given? + define_method(test_name, &block) + else + define_method(test_name) do + flunk "No implementation provided for #{name}" + end + end end end end diff --git a/vendor/rails/activesupport/lib/active_support/time_with_zone.rb b/vendor/rails/activesupport/lib/active_support/time_with_zone.rb index 75591b7..b7b8807 100644 --- a/vendor/rails/activesupport/lib/active_support/time_with_zone.rb +++ b/vendor/rails/activesupport/lib/active_support/time_with_zone.rb @@ -1,6 +1,6 @@ require 'tzinfo' module ActiveSupport - # A Time-like class that can represent a time in any time zone. Necessary because standard Ruby Time instances are + # A Time-like class that can represent a time in any time zone. Necessary because standard Ruby Time instances are # limited to UTC and the system's ENV['TZ'] zone. # # You shouldn't ever need to create a TimeWithZone instance directly via new -- instead, Rails provides the methods @@ -32,12 +32,12 @@ module ActiveSupport class TimeWithZone include Comparable attr_reader :time_zone - + def initialize(utc_time, time_zone, local_time = nil, period = nil) @utc, @time_zone, @time = utc_time, time_zone, local_time @period = @utc ? period : get_period_and_ensure_valid_local_time end - + # Returns a Time or DateTime instance that represents the time in +time_zone+. def time @time ||= period.to_local(@utc) @@ -51,7 +51,7 @@ def utc alias_method :getgm, :utc alias_method :getutc, :utc alias_method :gmtime, :utc - + # Returns the underlying TZInfo::TimezonePeriod. def period @period ||= time_zone.period_for_utc(@utc) @@ -62,38 +62,38 @@ def in_time_zone(new_zone = ::Time.zone) return self if time_zone == new_zone utc.in_time_zone(new_zone) end - + # Returns a Time.local() instance of the simultaneous time in your system's ENV['TZ'] zone def localtime utc.getlocal end alias_method :getlocal, :localtime - + def dst? period.dst? end alias_method :isdst, :dst? - + def utc? time_zone.name == 'UTC' end alias_method :gmt?, :utc? - + def utc_offset period.utc_total_offset end alias_method :gmt_offset, :utc_offset alias_method :gmtoff, :utc_offset - + def formatted_offset(colon = true, alternate_utc_string = nil) utc? && alternate_utc_string || utc_offset.to_utc_offset_s(colon) end - + # Time uses +zone+ to display the time zone abbreviation, so we're duck-typing it. def zone period.zone_identifier.to_s end - + def inspect "#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}" end @@ -122,7 +122,7 @@ def to_json(options = nil) %("#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}") end end - + def to_yaml(options = {}) if options.kind_of?(YAML::Emitter) utc.to_yaml(options) @@ -130,19 +130,19 @@ def to_yaml(options = {}) time.to_yaml(options).gsub('Z', formatted_offset(true, 'Z')) end end - + def httpdate utc.httpdate end - + def rfc2822 to_s(:rfc822) end alias_method :rfc822, :rfc2822 - + # :db format outputs time in UTC; all others output time in local. # Uses TimeWithZone's +strftime+, so %Z and %z work correctly. - def to_s(format = :default) + def to_s(format = :default) return utc.to_s(format) if format == :db if formatter = ::Time::DATE_FORMATS[format] formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter) @@ -150,27 +150,39 @@ def to_s(format = :default) "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby 1.9 Time#to_s format end end - + # Replaces %Z and %z directives with +zone+ and +formatted_offset+, respectively, before passing to # Time#strftime, so that zone information is correct def strftime(format) format = format.gsub('%Z', zone).gsub('%z', formatted_offset(false)) time.strftime(format) end - + # Use the time in UTC for comparisons. def <=>(other) utc <=> other end - + def between?(min, max) utc.between?(min, max) end - + + def past? + utc.past? + end + + def today? + time.today? + end + + def future? + utc.future? + end + def eql?(other) utc == other end - + def +(other) # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time, # otherwise move forward from #utc, for accuracy when moving across DST boundaries @@ -194,7 +206,7 @@ def -(other) result.in_time_zone(time_zone) end end - + def since(other) # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time, # otherwise move forward from #utc, for accuracy when moving across DST boundaries @@ -204,7 +216,7 @@ def since(other) utc.since(other).in_time_zone(time_zone) end end - + def ago(other) since(-other) end @@ -218,53 +230,53 @@ def advance(options) utc.advance(options).in_time_zone(time_zone) end end - - %w(year mon month day mday hour min sec).each do |method_name| + + %w(year mon month day mday wday yday hour min sec to_date).each do |method_name| class_eval <<-EOV def #{method_name} time.#{method_name} end EOV end - + def usec time.respond_to?(:usec) ? time.usec : 0 end - + def to_a [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone] end - + def to_f utc.to_f - end - + end + def to_i utc.to_i end alias_method :hash, :to_i alias_method :tv_sec, :to_i - + # A TimeWithZone acts like a Time, so just return +self+. def to_time self end - + def to_datetime utc.to_datetime.new_offset(Rational(utc_offset, 86_400)) end - + # So that +self+ acts_like?(:time). def acts_like_time? true end - + # Say we're a Time to thwart type checking. def is_a?(klass) klass == ::Time || super end alias_method :kind_of?, :is_a? - + # Neuter freeze because freezing can cause problems with lazy loading of attributes. def freeze self @@ -273,7 +285,7 @@ def freeze def marshal_dump [utc, time_zone.name, time] end - + def marshal_load(variables) initialize(variables[0].utc, ::Time.__send__(:get_zone, variables[1]), variables[2].utc) end @@ -290,10 +302,10 @@ def method_missing(sym, *args, &block) result = time.__send__(sym, *args, &block) result.acts_like?(:time) ? self.class.new(nil, time_zone, result) : result end - - private + + private def get_period_and_ensure_valid_local_time - # we don't want a Time.local instance enforcing its own DST rules as well, + # we don't want a Time.local instance enforcing its own DST rules as well, # so transfer time values to a utc constructor if necessary @time = transfer_time_values_to_utc_constructor(@time) unless @time.utc? begin @@ -304,11 +316,11 @@ def get_period_and_ensure_valid_local_time retry end end - + def transfer_time_values_to_utc_constructor(time) ::Time.utc_time(time.year, time.month, time.day, time.hour, time.min, time.sec, time.respond_to?(:usec) ? time.usec : 0) end - + def duration_of_variable_length?(obj) ActiveSupport::Duration === obj && obj.parts.flatten.detect {|p| [:years, :months, :days].include? p } end diff --git a/vendor/rails/activesupport/lib/active_support/values/time_zone.rb b/vendor/rails/activesupport/lib/active_support/values/time_zone.rb index 788d40b..a294dd0 100644 --- a/vendor/rails/activesupport/lib/active_support/values/time_zone.rb +++ b/vendor/rails/activesupport/lib/active_support/values/time_zone.rb @@ -124,7 +124,7 @@ class TimeZone "Kathmandu" => "Asia/Katmandu", "Astana" => "Asia/Dhaka", "Dhaka" => "Asia/Dhaka", - "Sri Jayawardenepura" => "Asia/Dhaka", + "Sri Jayawardenepura" => "Asia/Colombo", "Almaty" => "Asia/Almaty", "Novosibirsk" => "Asia/Novosibirsk", "Rangoon" => "Asia/Rangoon", diff --git a/vendor/rails/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/definitions/Asia/Colombo.rb b/vendor/rails/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/definitions/Asia/Colombo.rb new file mode 100644 index 0000000..f6531fa --- /dev/null +++ b/vendor/rails/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/definitions/Asia/Colombo.rb @@ -0,0 +1,30 @@ +require 'tzinfo/timezone_definition' + +module TZInfo + module Definitions + module Asia + module Colombo + include TimezoneDefinition + + timezone 'Asia/Colombo' do |tz| + tz.offset :o0, 19164, 0, :LMT + tz.offset :o1, 19172, 0, :MMT + tz.offset :o2, 19800, 0, :IST + tz.offset :o3, 19800, 1800, :IHST + tz.offset :o4, 19800, 3600, :IST + tz.offset :o5, 23400, 0, :LKT + tz.offset :o6, 21600, 0, :LKT + + tz.transition 1879, 12, :o1, 17335550003, 7200 + tz.transition 1905, 12, :o2, 52211763607, 21600 + tz.transition 1942, 1, :o3, 116657485, 48 + tz.transition 1942, 8, :o4, 9722413, 4 + tz.transition 1945, 10, :o2, 38907909, 16 + tz.transition 1996, 5, :o5, 832962600 + tz.transition 1996, 10, :o6, 846266400 + tz.transition 2006, 4, :o2, 1145039400 + end + end + end + end +end diff --git a/vendor/rails/activesupport/test/core_ext/date_ext_test.rb b/vendor/rails/activesupport/test/core_ext/date_ext_test.rb index 0f3cf4c..b53c754 100644 --- a/vendor/rails/activesupport/test/core_ext/date_ext_test.rb +++ b/vendor/rails/activesupport/test/core_ext/date_ext_test.rb @@ -210,6 +210,29 @@ def test_xmlschema end end + uses_mocha 'past?, today? and future?' do + def test_today + Date.stubs(:current).returns(Date.new(2000, 1, 1)) + assert_equal false, Date.new(1999, 12, 31).today? + assert_equal true, Date.new(2000,1,1).today? + assert_equal false, Date.new(2000,1,2).today? + end + + def test_past + Date.stubs(:current).returns(Date.new(2000, 1, 1)) + assert_equal true, Date.new(1999, 12, 31).past? + assert_equal false, Date.new(2000,1,1).past? + assert_equal false, Date.new(2000,1,2).past? + end + + def test_future + Date.stubs(:current).returns(Date.new(2000, 1, 1)) + assert_equal false, Date.new(1999, 12, 31).future? + assert_equal false, Date.new(2000,1,1).future? + assert_equal true, Date.new(2000,1,2).future? + end + end + uses_mocha 'TestDateCurrent' do def test_current_returns_date_today_when_zone_default_not_set with_env_tz 'US/Central' do diff --git a/vendor/rails/activesupport/test/core_ext/date_time_ext_test.rb b/vendor/rails/activesupport/test/core_ext/date_time_ext_test.rb index 854a3a0..be3cd8b 100644 --- a/vendor/rails/activesupport/test/core_ext/date_time_ext_test.rb +++ b/vendor/rails/activesupport/test/core_ext/date_time_ext_test.rb @@ -207,6 +207,81 @@ def test_xmlschema assert_match(/^2080-02-28T15:15:10-06:?00$/, DateTime.civil(2080, 2, 28, 15, 15, 10, -0.25).xmlschema) end + uses_mocha 'Test DateTime past?, today? and future?' do + def test_today_with_offset + Date.stubs(:current).returns(Date.new(2000, 1, 1)) + assert_equal false, DateTime.civil(1999,12,31,23,59,59, Rational(-18000, 86400)).today? + assert_equal true, DateTime.civil(2000,1,1,0,0,0, Rational(-18000, 86400)).today? + assert_equal true, DateTime.civil(2000,1,1,23,59,59, Rational(-18000, 86400)).today? + assert_equal false, DateTime.civil(2000,1,2,0,0,0, Rational(-18000, 86400)).today? + end + + def test_today_without_offset + Date.stubs(:current).returns(Date.new(2000, 1, 1)) + assert_equal false, DateTime.civil(1999,12,31,23,59,59).today? + assert_equal true, DateTime.civil(2000,1,1,0).today? + assert_equal true, DateTime.civil(2000,1,1,23,59,59).today? + assert_equal false, DateTime.civil(2000,1,2,0).today? + end + + def test_past_with_offset + DateTime.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) + assert_equal true, DateTime.civil(2005,2,10,15,30,44, Rational(-18000, 86400)).past? + assert_equal false, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400)).past? + assert_equal false, DateTime.civil(2005,2,10,15,30,46, Rational(-18000, 86400)).past? + end + + def test_past_without_offset + DateTime.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) + assert_equal true, DateTime.civil(2005,2,10,20,30,44).past? + assert_equal false, DateTime.civil(2005,2,10,20,30,45).past? + assert_equal false, DateTime.civil(2005,2,10,20,30,46).past? + end + + def test_future_with_offset + DateTime.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) + assert_equal false, DateTime.civil(2005,2,10,15,30,44, Rational(-18000, 86400)).future? + assert_equal false, DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400)).future? + assert_equal true, DateTime.civil(2005,2,10,15,30,46, Rational(-18000, 86400)).future? + end + + def test_future_without_offset + DateTime.stubs(:current).returns(DateTime.civil(2005,2,10,15,30,45, Rational(-18000, 86400))) + assert_equal false, DateTime.civil(2005,2,10,20,30,44).future? + assert_equal false, DateTime.civil(2005,2,10,20,30,45).future? + assert_equal true, DateTime.civil(2005,2,10,20,30,46).future? + end + end + + uses_mocha 'TestDateTimeCurrent' do + def test_current_returns_date_today_when_zone_default_not_set + with_env_tz 'US/Eastern' do + Time.stubs(:now).returns Time.local(1999, 12, 31, 23, 59, 59) + assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current + end + end + + def test_current_returns_time_zone_today_when_zone_default_set + Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + with_env_tz 'US/Eastern' do + Time.stubs(:now).returns Time.local(1999, 12, 31, 23, 59, 59) + assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current + end + ensure + Time.zone_default = nil + end + end + + def test_current_without_time_zone + assert DateTime.current.is_a?(DateTime) + end + + def test_current_with_time_zone + with_env_tz 'US/Eastern' do + assert DateTime.current.is_a?(DateTime) + end + end + def test_acts_like_time assert DateTime.new.acts_like_time? end diff --git a/vendor/rails/activesupport/test/core_ext/hash_ext_test.rb b/vendor/rails/activesupport/test/core_ext/hash_ext_test.rb index 7a414e9..44d48e7 100644 --- a/vendor/rails/activesupport/test/core_ext/hash_ext_test.rb +++ b/vendor/rails/activesupport/test/core_ext/hash_ext_test.rb @@ -341,6 +341,20 @@ def test_except assert_equal expected, original.except!(:c) assert_equal expected, original end + + def test_except_with_original_frozen + original = { :a => 'x', :b => 'y' } + original.freeze + assert_nothing_raised { original.except(:a) } + end + + uses_mocha 'except with expectation' do + def test_except_with_mocha_expectation_on_original + original = { :a => 'x', :b => 'y' } + original.expects(:delete).never + original.except(:a) + end + end end class IWriteMyOwnXML diff --git a/vendor/rails/activesupport/test/core_ext/string_ext_test.rb b/vendor/rails/activesupport/test/core_ext/string_ext_test.rb index c9f959e..c0decf2 100644 --- a/vendor/rails/activesupport/test/core_ext/string_ext_test.rb +++ b/vendor/rails/activesupport/test/core_ext/string_ext_test.rb @@ -201,3 +201,9 @@ def test_each_char_with_utf8_string_when_kcode_is_utf8 end end end + +class StringBehaviourTest < Test::Unit::TestCase + def test_acts_like_string + assert 'Bambi'.acts_like_string? + end +end \ No newline at end of file diff --git a/vendor/rails/activesupport/test/core_ext/time_ext_test.rb b/vendor/rails/activesupport/test/core_ext/time_ext_test.rb index c1bdee4..7e55405 100644 --- a/vendor/rails/activesupport/test/core_ext/time_ext_test.rb +++ b/vendor/rails/activesupport/test/core_ext/time_ext_test.rb @@ -563,6 +563,74 @@ def test_xmlschema_is_available assert_nothing_raised { Time.now.xmlschema } end + uses_mocha 'Test Time past?, today? and future?' do + def test_today_with_time_local + Date.stubs(:current).returns(Date.new(2000, 1, 1)) + assert_equal false, Time.local(1999,12,31,23,59,59).today? + assert_equal true, Time.local(2000,1,1,0).today? + assert_equal true, Time.local(2000,1,1,23,59,59).today? + assert_equal false, Time.local(2000,1,2,0).today? + end + + def test_today_with_time_utc + Date.stubs(:current).returns(Date.new(2000, 1, 1)) + assert_equal false, Time.utc(1999,12,31,23,59,59).today? + assert_equal true, Time.utc(2000,1,1,0).today? + assert_equal true, Time.utc(2000,1,1,23,59,59).today? + assert_equal false, Time.utc(2000,1,2,0).today? + end + + def test_past_with_time_current_as_time_local + with_env_tz 'US/Eastern' do + Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) + assert_equal true, Time.local(2005,2,10,15,30,44).past? + assert_equal false, Time.local(2005,2,10,15,30,45).past? + assert_equal false, Time.local(2005,2,10,15,30,46).past? + assert_equal true, Time.utc(2005,2,10,20,30,44).past? + assert_equal false, Time.utc(2005,2,10,20,30,45).past? + assert_equal false, Time.utc(2005,2,10,20,30,46).past? + end + end + + def test_past_with_time_current_as_time_with_zone + with_env_tz 'US/Eastern' do + twz = Time.utc(2005,2,10,15,30,45).in_time_zone('Central Time (US & Canada)') + Time.stubs(:current).returns(twz) + assert_equal true, Time.local(2005,2,10,10,30,44).past? + assert_equal false, Time.local(2005,2,10,10,30,45).past? + assert_equal false, Time.local(2005,2,10,10,30,46).past? + assert_equal true, Time.utc(2005,2,10,15,30,44).past? + assert_equal false, Time.utc(2005,2,10,15,30,45).past? + assert_equal false, Time.utc(2005,2,10,15,30,46).past? + end + end + + def test_future_with_time_current_as_time_local + with_env_tz 'US/Eastern' do + Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) + assert_equal false, Time.local(2005,2,10,15,30,44).future? + assert_equal false, Time.local(2005,2,10,15,30,45).future? + assert_equal true, Time.local(2005,2,10,15,30,46).future? + assert_equal false, Time.utc(2005,2,10,20,30,44).future? + assert_equal false, Time.utc(2005,2,10,20,30,45).future? + assert_equal true, Time.utc(2005,2,10,20,30,46).future? + end + end + + def test_future_with_time_current_as_time_with_zone + with_env_tz 'US/Eastern' do + twz = Time.utc(2005,2,10,15,30,45).in_time_zone('Central Time (US & Canada)') + Time.stubs(:current).returns(twz) + assert_equal false, Time.local(2005,2,10,10,30,44).future? + assert_equal false, Time.local(2005,2,10,10,30,45).future? + assert_equal true, Time.local(2005,2,10,10,30,46).future? + assert_equal false, Time.utc(2005,2,10,15,30,44).future? + assert_equal false, Time.utc(2005,2,10,15,30,45).future? + assert_equal true, Time.utc(2005,2,10,15,30,46).future? + end + end + end + def test_acts_like_time assert Time.new.acts_like_time? end @@ -640,24 +708,24 @@ def test_marshaling_with_utc_instance assert_equal t, unmarshaled assert_equal t.zone, unmarshaled.zone end - - def test_marshaling_with_local_instance + + def test_marshaling_with_local_instance t = Time.local(2000) marshaled = Marshal.dump t unmarshaled = Marshal.load marshaled assert_equal t, unmarshaled assert_equal t.zone, unmarshaled.zone end - - def test_marshaling_with_frozen_utc_instance + + def test_marshaling_with_frozen_utc_instance t = Time.utc(2000).freeze marshaled = Marshal.dump t unmarshaled = Marshal.load marshaled assert_equal t, unmarshaled assert_equal t.zone, unmarshaled.zone end - - def test_marshaling_with_frozen_local_instance + + def test_marshaling_with_frozen_local_instance t = Time.local(2000).freeze marshaled = Marshal.dump t unmarshaled = Marshal.load marshaled diff --git a/vendor/rails/activesupport/test/core_ext/time_with_zone_test.rb b/vendor/rails/activesupport/test/core_ext/time_with_zone_test.rb index dfe0448..72b540e 100644 --- a/vendor/rails/activesupport/test/core_ext/time_with_zone_test.rb +++ b/vendor/rails/activesupport/test/core_ext/time_with_zone_test.rb @@ -152,6 +152,50 @@ def test_between? assert_equal false, @twz.between?(Time.utc(2000,1,1,0,0,1), Time.utc(2000,1,1,0,0,2)) end + uses_mocha 'TimeWithZone past?, today? and future?' do + def test_today + Date.stubs(:current).returns(Date.new(2000, 1, 1)) + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(1999,12,31,23,59,59) ).today? + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,1,0) ).today? + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,1,23,59,59) ).today? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.utc(2000,1,2,0) ).today? + end + + def test_past_with_time_current_as_time_local + with_env_tz 'US/Eastern' do + Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).past? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).past? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).past? + end + end + + def test_past_with_time_current_as_time_with_zone + twz = ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45) ) + Time.stubs(:current).returns(twz) + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).past? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).past? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).past? + end + + def test_future_with_time_current_as_time_local + with_env_tz 'US/Eastern' do + Time.stubs(:current).returns(Time.local(2005,2,10,15,30,45)) + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).future? + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).future? + end + end + + def future_with_time_current_as_time_with_zone + twz = ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45) ) + Time.stubs(:current).returns(twz) + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,44)).future? + assert_equal false, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,45)).future? + assert_equal true, ActiveSupport::TimeWithZone.new( nil, @time_zone, Time.local(2005,2,10,15,30,46)).future? + end + end + def test_eql? assert @twz.eql?(Time.utc(2000)) assert @twz.eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) ) @@ -353,7 +397,7 @@ def test_method_missing_with_non_time_return_value def test_date_part_value_methods silence_warnings do # silence warnings raised by tzinfo gem twz = ActiveSupport::TimeWithZone.new(Time.utc(1999,12,31,19,18,17,500), @time_zone) - twz.stubs(:method_missing).returns(nil) #ensure these methods are defined directly on class + twz.expects(:method_missing).never assert_equal 1999, twz.year assert_equal 12, twz.month assert_equal 31, twz.day @@ -361,6 +405,8 @@ def test_date_part_value_methods assert_equal 18, twz.min assert_equal 17, twz.sec assert_equal 500, twz.usec + assert_equal 5, twz.wday + assert_equal 365, twz.yday end end end @@ -538,7 +584,7 @@ def test_advance_1_day_across_spring_dst_transition assert_equal "Sun, 02 Apr 2006 10:30:01 EDT -04:00", twz.since(1.days + 1.second).inspect assert_equal "Sun, 02 Apr 2006 10:30:01 EDT -04:00", (twz + 1.days + 1.second).inspect end - + def test_advance_1_day_across_spring_dst_transition_backwards twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,10,30)) # In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long @@ -548,7 +594,7 @@ def test_advance_1_day_across_spring_dst_transition_backwards assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 1.days).inspect assert_equal "Sat, 01 Apr 2006 10:30:01 EST -05:00", twz.ago(1.days - 1.second).inspect end - + def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_spring_dst_transition twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,1,10,30)) # In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long @@ -565,7 +611,7 @@ def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_sp assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.since(24.hours).inspect assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.advance(:hours => 24).inspect end - + def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_spring_dst_transition_backwards twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,11,30)) # In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long @@ -582,7 +628,7 @@ def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_sp assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(24.hours).inspect assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(:hours => -24).inspect end - + def test_advance_1_day_across_fall_dst_transition twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30)) # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long @@ -593,7 +639,7 @@ def test_advance_1_day_across_fall_dst_transition assert_equal "Sun, 29 Oct 2006 10:30:01 EST -05:00", twz.since(1.days + 1.second).inspect assert_equal "Sun, 29 Oct 2006 10:30:01 EST -05:00", (twz + 1.days + 1.second).inspect end - + def test_advance_1_day_across_fall_dst_transition_backwards twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,29,10,30)) # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long @@ -603,7 +649,7 @@ def test_advance_1_day_across_fall_dst_transition_backwards assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 1.days).inspect assert_equal "Sat, 28 Oct 2006 10:30:01 EDT -04:00", twz.ago(1.days - 1.second).inspect end - + def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_fall_dst_transition twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30)) # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long @@ -620,7 +666,7 @@ def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_fa assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.since(24.hours).inspect assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.advance(:hours => 24).inspect end - + def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_fall_dst_transition_backwards twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,29,9,30)) # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long @@ -669,7 +715,7 @@ def test_advance_1_month_across_fall_dst_transition_backwards assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(1.month).inspect assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 1.month).inspect end - + def test_advance_1_year twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008,2,15,10,30)) assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", twz.advance(:years => 1).inspect @@ -679,7 +725,7 @@ def test_advance_1_year assert_equal "Thu, 15 Feb 2007 10:30:00 EST -05:00", twz.years_ago(1).inspect assert_equal "Thu, 15 Feb 2007 10:30:00 EST -05:00", (twz - 1.year).inspect end - + def test_advance_1_year_during_dst twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008,7,15,10,30)) assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", twz.advance(:years => 1).inspect @@ -689,6 +735,14 @@ def test_advance_1_year_during_dst assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", twz.years_ago(1).inspect assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", (twz - 1.year).inspect end + + protected + def with_env_tz(new_tz = 'US/Eastern') + old_tz, ENV['TZ'] = ENV['TZ'], new_tz + yield + ensure + old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + end end class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase diff --git a/vendor/rails/activesupport/test/inflector_test.rb b/vendor/rails/activesupport/test/inflector_test.rb index 8eebe1b..f304844 100644 --- a/vendor/rails/activesupport/test/inflector_test.rb +++ b/vendor/rails/activesupport/test/inflector_test.rb @@ -98,6 +98,12 @@ def test_tableize end end + def test_parameterize + StringToParameterized.each do |some_string, parameterized_string| + assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string)) + end + end + def test_classify ClassNameToTableName.each do |class_name, table_name| assert_equal(class_name, ActiveSupport::Inflector.classify(table_name)) diff --git a/vendor/rails/activesupport/test/inflector_test_cases.rb b/vendor/rails/activesupport/test/inflector_test_cases.rb index a9dd83a..8057809 100644 --- a/vendor/rails/activesupport/test/inflector_test_cases.rb +++ b/vendor/rails/activesupport/test/inflector_test_cases.rb @@ -142,6 +142,14 @@ module InflectorTestCases "NodeChild" => "node_children" } + StringToParameterized = { + "Donald E. Knuth" => "donald-e-knuth", + "Random text with *(bad)* characters" => "random-text-with-bad-characters", + "Malmö" => "malmo", + "Garçons" => "garcons", + "Allow_Under_Scores" => "allow_under_scores" + } + UnderscoreToHuman = { "employee_salary" => "Employee salary", "employee_id" => "Employee", diff --git a/vendor/rails/activesupport/test/multibyte_chars_test.rb b/vendor/rails/activesupport/test/multibyte_chars_test.rb index 63cfadb..a87309b 100644 --- a/vendor/rails/activesupport/test/multibyte_chars_test.rb +++ b/vendor/rails/activesupport/test/multibyte_chars_test.rb @@ -167,6 +167,10 @@ def test_duck_typing assert_equal false, 'test'.chars.respond_to?(:a_method_that_doesnt_exist) end + def test_acts_like_string + assert 'Bambi'.chars.acts_like_string? + end + protected def with_kcode(kcode) diff --git a/vendor/rails/railties/CHANGELOG b/vendor/rails/railties/CHANGELOG index 3a276d5..51d66e4 100644 --- a/vendor/rails/railties/CHANGELOG +++ b/vendor/rails/railties/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Fixed that sqlite would report "db/development.sqlite3 already exists" whether true or not on db:create #614 [Antonio Cangiano] + * Added config.threadsafe! to toggle allow concurrency settings and disable the dependency loader [Josh Peek] * Turn cache_classes on by default [Josh Peek] diff --git a/vendor/rails/railties/Rakefile b/vendor/rails/railties/Rakefile index 174c85b..ec2fe85 100644 --- a/vendor/rails/railties/Rakefile +++ b/vendor/rails/railties/Rakefile @@ -272,20 +272,49 @@ Rake::RDocTask.new { |rdoc| rdoc.rdoc_files.include('lib/commands/**/*.rb') } -guides = ['securing_rails_applications', 'testing_rails_applications', 'creating_plugins'] -guides_html_files = [] -guides.each do |guide_name| - input = "doc/guides/#{guide_name}/#{guide_name}.txt" - output = "doc/guides/#{guide_name}/#{guide_name}.html" +# In this array, one defines the guides for which HTML output should be +# generated. Specify the folder names of the guides. If the .txt filename +# doesn't equal its folder name, then specify a hash: { 'folder_name' => 'basename' } +guides = [ + 'getting_started_with_rails', + 'securing_rails_applications', + 'testing_rails_applications', + 'creating_plugins', + 'migrations', + { 'routing' => 'routing_outside_in' }, + { 'forms' =>'form_helpers' }, + { 'activerecord' => 'association_basics' }, + { 'debugging' => 'debugging_rails_applications' } +] + +guides_html_files = [] # autogenerated from the 'guides' variable. +guides.each do |entry| + if entry.is_a?(Hash) + guide_folder = entry.keys.first + guide_name = entry.values.first + else + guide_folder = entry + guide_name = entry + end + input = "doc/guides/#{guide_folder}/#{guide_name}.txt" + output = "doc/guides/#{guide_folder}/#{guide_name}.html" guides_html_files << output - file output => Dir["doc/guides/#{guide_name}/*.txt"] do - sh "mizuho", input, "--template", "manualsonrails", "--multi-page", - "--icons-dir", "../icons" + task output => Dir["doc/guides/#{guide_folder}/*.txt"] do + ENV['MANUALSONRAILS_INDEX_URL'] = '../index.html' + ENV.delete('MANUALSONRAILS_TOC') + sh "mizuho", input, "--template", "manualsonrails", "--icons-dir", "../icons" end end +task 'doc/guides/index.html' => 'doc/guides/index.txt' do + ENV.delete('MANUALSONRAILS_INDEX_URL') + ENV['MANUALSONRAILS_TOC'] = 'no' + sh "mizuho", 'doc/guides/index.txt', "--template", "manualsonrails", "--icons-dir", "icons" +end + desc "Generate HTML output for the guides" task :generate_guides => guides_html_files +task :generate_guides => 'doc/guides/index.html' # Generate GEM ---------------------------------------------------------------------------- diff --git a/vendor/rails/railties/lib/commands/console.rb b/vendor/rails/railties/lib/commands/console.rb index 2b9d92f..63df834 100644 --- a/vendor/rails/railties/lib/commands/console.rb +++ b/vendor/rails/railties/lib/commands/console.rb @@ -1,11 +1,13 @@ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' require 'optparse' + options = { :sandbox => false, :irb => irb } OptionParser.new do |opt| opt.banner = "Usage: console [environment] [options]" opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |v| options[:sandbox] = v } opt.on("--irb=[#{irb}]", 'Invoke a different irb.') { |v| options[:irb] = v } + opt.on("--debugger", 'Enable ruby-debugging for the console.') { |v| options[:debugger] = v } opt.parse!(ARGV) end @@ -15,6 +17,17 @@ libs << " -r console_sandbox" if options[:sandbox] libs << " -r console_with_helpers" +if options[:debugger] + begin + require 'ruby-debug' + libs << " -r ruby-debug" + puts "=> Debugger enabled" + rescue Exception + puts "You need to install ruby-debug to run the console in debugging mode. With gems, use 'gem install ruby-debug'" + exit + end +end + ENV['RAILS_ENV'] = case ARGV.first when "p"; "production" when "d"; "development" diff --git a/vendor/rails/railties/lib/commands/plugin.rb b/vendor/rails/railties/lib/commands/plugin.rb index 980244a..9ff4739 100644 --- a/vendor/rails/railties/lib/commands/plugin.rb +++ b/vendor/rails/railties/lib/commands/plugin.rb @@ -461,11 +461,11 @@ def options o.on("-r", "--root=DIR", String, "Set an explicit rails app directory.", - "Default: #{@rails_root}") { |@rails_root| self.environment = RailsEnvironment.new(@rails_root) } + "Default: #{@rails_root}") { |rails_root| @rails_root = rails_root; self.environment = RailsEnvironment.new(@rails_root) } o.on("-s", "--source=URL1,URL2", Array, - "Use the specified plugin repositories instead of the defaults.") { |@sources|} + "Use the specified plugin repositories instead of the defaults.") { |sources| @sources = sources} - o.on("-v", "--verbose", "Turn on verbose output.") { |$verbose| } + o.on("-v", "--verbose", "Turn on verbose output.") { |verbose| $verbose = verbose } o.on("-h", "--help", "Show this help message.") { puts o; exit } o.separator "" @@ -552,12 +552,12 @@ def options o.separator "Options:" o.separator "" o.on( "-s", "--source=URL1,URL2", Array, - "Use the specified plugin repositories.") {|@sources|} + "Use the specified plugin repositories.") {|sources| @sources = sources} o.on( "--local", - "List locally installed plugins.") {|@local| @remote = false} + "List locally installed plugins.") {|local| @local, @remote = local, false} o.on( "--remote", "List remotely available plugins. This is the default behavior", - "unless --local is provided.") {|@remote|} + "unless --local is provided.") {|remote| @remote = remote} end end @@ -598,7 +598,7 @@ def options o.separator "Options:" o.separator "" o.on( "-c", "--check", - "Report status of repository.") { |@sources|} + "Report status of repository.") { |sources| @sources = sources} end end @@ -689,7 +689,7 @@ def options o.separator "Options:" o.separator "" o.on( "-l", "--list", - "List but don't prompt or add discovered repositories.") { |@list| @prompt = !@list } + "List but don't prompt or add discovered repositories.") { |list| @list, @prompt = list, !@list } o.on( "-n", "--no-prompt", "Add all new repositories without prompting.") { |v| @prompt = !v } end @@ -901,7 +901,7 @@ class RecursiveHTTPFetcher def initialize(urls_to_fetch, level = 1, cwd = ".") @level = level @cwd = cwd - @urls_to_fetch = urls_to_fetch.to_a + @urls_to_fetch = RUBY_VERSION >= '1.9' ? urls_to_fetch.lines : urls_to_fetch.to_a @quiet = false end diff --git a/vendor/rails/railties/lib/commands/process/spawner.rb b/vendor/rails/railties/lib/commands/process/spawner.rb index dc00086..8bf47ab 100644 --- a/vendor/rails/railties/lib/commands/process/spawner.rb +++ b/vendor/rails/railties/lib/commands/process/spawner.rb @@ -181,10 +181,10 @@ def self.can_bind_to_custom_address? opts.on(" Options:") - opts.on("-p", "--port=number", Integer, "Starting port number (default: #{OPTIONS[:port]})") { |OPTIONS[:port]| } + opts.on("-p", "--port=number", Integer, "Starting port number (default: #{OPTIONS[:port]})") { |v| OPTIONS[:port] = v } if spawner_class.can_bind_to_custom_address? - opts.on("-a", "--address=ip", String, "Bind to IP address (default: #{OPTIONS[:address]})") { |OPTIONS[:address]| } + opts.on("-a", "--address=ip", String, "Bind to IP address (default: #{OPTIONS[:address]})") { |v| OPTIONS[:address] = v } end opts.on("-p", "--port=number", Integer, "Starting port number (default: #{OPTIONS[:port]})") { |v| OPTIONS[:port] = v } diff --git a/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb index 1b7bcfe..4de9407 100644 --- a/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb +++ b/vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class <%= class_name %>Test < ActionMailer::TestCase - tests <%= class_name %> <% for action in actions -%> test "<%= action %>" do @expected.subject = '<%= class_name %>#<%= action %>' diff --git a/vendor/rails/railties/lib/tasks/databases.rake b/vendor/rails/railties/lib/tasks/databases.rake index 21c81b3..1431aa6 100644 --- a/vendor/rails/railties/lib/tasks/databases.rake +++ b/vendor/rails/railties/lib/tasks/databases.rake @@ -28,8 +28,24 @@ namespace :db do def create_database(config) begin - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection + if config['adapter'] =~ /sqlite/ + if File.exist?(config['database']) + $stderr.puts "#{config['database']} already exists" + else + begin + # Create the SQLite database + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.connection + rescue + $stderr.puts $!, *($!.backtrace) + $stderr.puts "Couldn't create database for #{config.inspect}" + end + end + return # Skip the else clause of begin/rescue + else + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.connection + end rescue case config['adapter'] when 'mysql' @@ -52,10 +68,6 @@ namespace :db do $stderr.puts $!, *($!.backtrace) $stderr.puts "Couldn't create database for #{config.inspect}" end - when 'sqlite' - `sqlite "#{config['database']}"` - when 'sqlite3' - `sqlite3 "#{config['database']}"` end else $stderr.puts "#{config['database']} already exists" @@ -101,8 +113,16 @@ namespace :db do end namespace :migrate do - desc 'Rollbacks the database one migration and re migrate up. If you want to rollback more than one step, define STEP=x' - task :redo => [ 'db:rollback', 'db:migrate' ] + desc 'Rollbacks the database one migration and re migrate up. If you want to rollback more than one step, define STEP=x. Target specific version with VERSION=x.' + task :redo => :environment do + if ENV["VERSION"] + Rake::Task["db:migrate:down"].invoke + Rake::Task["db:migrate:up"].invoke + else + Rake::Task["db:rollback"].invoke + Rake::Task["db:migrate"].invoke + end + end desc 'Resets your database using your migrations for the current environment' task :reset => ["db:drop", "db:create", "db:migrate"]