aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrick <technoweenie@gmail.com>2008-08-26 11:53:33 -0700
committerrick <technoweenie@gmail.com>2008-08-26 11:53:33 -0700
commit0aef9d1a2651fa0acd2adcd2de308eeb0ec8cdd2 (patch)
tree1a782151632dd80c8a18c3960536bdf8643debe3
parent0a6d75dedd79407376aae1f01302164dfd3e44b6 (diff)
parent229eedfda87a7706dbb5e3e51af8707b3adae375 (diff)
downloadrails-0aef9d1a2651fa0acd2adcd2de308eeb0ec8cdd2.tar.gz
rails-0aef9d1a2651fa0acd2adcd2de308eeb0ec8cdd2.tar.bz2
rails-0aef9d1a2651fa0acd2adcd2de308eeb0ec8cdd2.zip
Merge branch 'master' of git@github.com:rails/rails
-rw-r--r--.gitignore1
-rw-r--r--[-rwxr-xr-x]actionmailer/README0
-rw-r--r--[-rwxr-xr-x]actionmailer/Rakefile0
-rw-r--r--[-rwxr-xr-x]actionmailer/lib/action_mailer.rb12
-rw-r--r--actionmailer/lib/action_mailer/base.rb67
-rw-r--r--actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/address.rb4
-rw-r--r--actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/header.rb2
-rw-r--r--actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/interface.rb2
-rw-r--r--actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/mail.rb4
-rw-r--r--actionmailer/test/abstract_unit.rb18
-rw-r--r--actionmailer/test/fixtures/test_mailer/signed_up.html.erb (renamed from actionmailer/test/fixtures/test_mailer/signed_up.erb)0
-rw-r--r--actionmailer/test/mail_render_test.rb12
-rw-r--r--[-rwxr-xr-x]actionmailer/test/mail_service_test.rb37
-rw-r--r--actionpack/CHANGELOG30
-rw-r--r--actionpack/README18
-rw-r--r--[-rwxr-xr-x]actionpack/lib/action_controller.rb15
-rw-r--r--actionpack/lib/action_controller/assertions/response_assertions.rb4
-rw-r--r--actionpack/lib/action_controller/assertions/routing_assertions.rb44
-rw-r--r--actionpack/lib/action_controller/assertions/selector_assertions.rb32
-rw-r--r--[-rwxr-xr-x]actionpack/lib/action_controller/base.rb105
-rw-r--r--actionpack/lib/action_controller/caching/fragments.rb6
-rw-r--r--actionpack/lib/action_controller/cgi_process.rb45
-rw-r--r--actionpack/lib/action_controller/cookies.rb10
-rw-r--r--actionpack/lib/action_controller/dispatcher.rb24
-rw-r--r--actionpack/lib/action_controller/filters.rb19
-rw-r--r--actionpack/lib/action_controller/headers.rb30
-rw-r--r--actionpack/lib/action_controller/integration.rb100
-rw-r--r--actionpack/lib/action_controller/polymorphic_routes.rb16
-rw-r--r--actionpack/lib/action_controller/rack_process.rb156
-rw-r--r--[-rwxr-xr-x]actionpack/lib/action_controller/request.rb204
-rw-r--r--actionpack/lib/action_controller/request_forgery_protection.rb2
-rw-r--r--[-rwxr-xr-x]actionpack/lib/action_controller/request_profiler.rb0
-rw-r--r--actionpack/lib/action_controller/rescue.rb30
-rw-r--r--actionpack/lib/action_controller/resources.rb63
-rw-r--r--[-rwxr-xr-x]actionpack/lib/action_controller/response.rb151
-rw-r--r--actionpack/lib/action_controller/routing.rb58
-rw-r--r--actionpack/lib/action_controller/routing/builder.rb44
-rw-r--r--actionpack/lib/action_controller/routing/optimisations.rb25
-rw-r--r--actionpack/lib/action_controller/routing/recognition_optimisation.rb40
-rw-r--r--actionpack/lib/action_controller/routing/route.rb309
-rw-r--r--actionpack/lib/action_controller/routing/route_set.rb7
-rw-r--r--actionpack/lib/action_controller/routing/routing_ext.rb1
-rw-r--r--actionpack/lib/action_controller/routing/segments.rb37
-rw-r--r--actionpack/lib/action_controller/session/cookie_store.rb2
-rwxr-xr-x[-rw-r--r--]actionpack/lib/action_controller/session/drb_server.rb14
-rw-r--r--actionpack/lib/action_controller/streaming.rb23
-rw-r--r--actionpack/lib/action_controller/templates/rescues/diagnostics.erb4
-rw-r--r--actionpack/lib/action_controller/templates/rescues/template_error.erb4
-rw-r--r--actionpack/lib/action_controller/test_case.rb63
-rw-r--r--actionpack/lib/action_controller/test_process.rb91
-rw-r--r--actionpack/lib/action_controller/url_rewriter.rb103
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb4
-rw-r--r--actionpack/lib/action_view.rb20
-rw-r--r--actionpack/lib/action_view/base.rb111
-rw-r--r--actionpack/lib/action_view/helpers.rb39
-rw-r--r--actionpack/lib/action_view/helpers/active_record_helper.rb71
-rw-r--r--actionpack/lib/action_view/helpers/asset_tag_helper.rb96
-rw-r--r--actionpack/lib/action_view/helpers/atom_feed_helper.rb24
-rw-r--r--actionpack/lib/action_view/helpers/cache_helper.rb2
-rw-r--r--actionpack/lib/action_view/helpers/capture_helper.rb17
-rw-r--r--[-rwxr-xr-x]actionpack/lib/action_view/helpers/date_helper.rb764
-rw-r--r--actionpack/lib/action_view/helpers/debug_helper.rb33
-rw-r--r--actionpack/lib/action_view/helpers/form_country_helper.rb92
-rw-r--r--actionpack/lib/action_view/helpers/form_helper.rb73
-rw-r--r--actionpack/lib/action_view/helpers/form_options_helper.rb75
-rw-r--r--actionpack/lib/action_view/helpers/form_tag_helper.rb2
-rw-r--r--actionpack/lib/action_view/helpers/javascript_helper.rb21
-rw-r--r--actionpack/lib/action_view/helpers/javascripts/prototype.js304
-rw-r--r--actionpack/lib/action_view/helpers/number_helper.rb253
-rw-r--r--actionpack/lib/action_view/helpers/prototype_helper.rb416
-rw-r--r--actionpack/lib/action_view/helpers/sanitize_helper.rb87
-rw-r--r--actionpack/lib/action_view/helpers/scriptaculous_helper.rb2
-rw-r--r--actionpack/lib/action_view/helpers/tag_helper.rb20
-rw-r--r--actionpack/lib/action_view/helpers/text_helper.rb206
-rw-r--r--actionpack/lib/action_view/helpers/translation_helper.rb22
-rw-r--r--actionpack/lib/action_view/helpers/url_helper.rb99
-rw-r--r--actionpack/lib/action_view/locale/en-US.yml91
-rw-r--r--actionpack/lib/action_view/partials.rb140
-rw-r--r--actionpack/lib/action_view/paths.rb87
-rw-r--r--actionpack/lib/action_view/renderable.rb83
-rw-r--r--actionpack/lib/action_view/renderable_partial.rb2
-rw-r--r--actionpack/lib/action_view/template.rb19
-rw-r--r--actionpack/lib/action_view/template_error.rb2
-rw-r--r--actionpack/lib/action_view/template_handler.rb27
-rw-r--r--actionpack/lib/action_view/template_handlers.rb1
-rw-r--r--actionpack/lib/action_view/template_handlers/builder.rb10
-rw-r--r--actionpack/lib/action_view/template_handlers/compilable.rb20
-rw-r--r--actionpack/lib/action_view/template_handlers/erb.rb11
-rw-r--r--actionpack/lib/action_view/template_handlers/rjs.rb11
-rw-r--r--actionpack/lib/action_view/test_case.rb4
-rw-r--r--actionpack/test/abstract_unit.rb2
-rw-r--r--actionpack/test/activerecord/render_partial_with_record_identification_test.rb10
-rw-r--r--actionpack/test/controller/assert_select_test.rb33
-rw-r--r--actionpack/test/controller/base_test.rb20
-rw-r--r--actionpack/test/controller/caching_test.rb107
-rw-r--r--[-rwxr-xr-x]actionpack/test/controller/cgi_test.rb52
-rw-r--r--actionpack/test/controller/content_type_test.rb28
-rw-r--r--actionpack/test/controller/cookie_test.rb2
-rw-r--r--actionpack/test/controller/integration_test.rb90
-rw-r--r--actionpack/test/controller/layout_test.rb31
-rw-r--r--actionpack/test/controller/mime_responds_test.rb62
-rw-r--r--actionpack/test/controller/new_render_test.rb18
-rw-r--r--actionpack/test/controller/polymorphic_routes_test.rb22
-rw-r--r--actionpack/test/controller/rack_test.rb125
-rw-r--r--[-rwxr-xr-x]actionpack/test/controller/redirect_test.rb109
-rw-r--r--actionpack/test/controller/render_test.rb198
-rw-r--r--actionpack/test/controller/request_test.rb219
-rw-r--r--actionpack/test/controller/rescue_test.rb9
-rw-r--r--actionpack/test/controller/resources_test.rb20
-rw-r--r--actionpack/test/controller/routing_test.rb178
-rw-r--r--[-rwxr-xr-x]actionpack/test/controller/session/cookie_store_test.rb0
-rw-r--r--actionpack/test/controller/test_test.rb25
-rw-r--r--actionpack/test/controller/url_rewriter_test.rb55
-rw-r--r--actionpack/test/controller/view_paths_test.rb10
-rw-r--r--actionpack/test/fixtures/_top_level_partial.html.erb1
-rw-r--r--actionpack/test/fixtures/_top_level_partial_only.erb1
-rw-r--r--actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb3
-rw-r--r--actionpack/test/fixtures/functional_caching/formatted_fragment_cached.js.rjs6
-rw-r--r--actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder5
-rw-r--r--actionpack/test/fixtures/test/_counter.html.erb1
-rw-r--r--actionpack/test/fixtures/test/_layout_for_block_with_args.html.erb3
-rw-r--r--actionpack/test/fixtures/test/implicit_content_type.atom.builder2
-rw-r--r--actionpack/test/fixtures/test/using_layout_around_block_with_args.html.erb1
-rw-r--r--actionpack/test/template/active_record_helper_i18n_test.rb46
-rw-r--r--actionpack/test/template/active_record_helper_test.rb48
-rw-r--r--actionpack/test/template/asset_tag_helper_test.rb56
-rw-r--r--actionpack/test/template/atom_feed_helper_test.rb37
-rw-r--r--actionpack/test/template/compiled_templates_test.rb47
-rw-r--r--actionpack/test/template/date_helper_i18n_test.rb113
-rw-r--r--[-rwxr-xr-x]actionpack/test/template/date_helper_test.rb437
-rw-r--r--actionpack/test/template/form_country_helper_test.rb1549
-rw-r--r--actionpack/test/template/form_helper_test.rb116
-rw-r--r--actionpack/test/template/form_options_helper_test.rb1539
-rw-r--r--actionpack/test/template/form_tag_helper_test.rb6
-rw-r--r--actionpack/test/template/javascript_helper_test.rb6
-rw-r--r--actionpack/test/template/number_helper_i18n_test.rb54
-rw-r--r--actionpack/test/template/number_helper_test.rb56
-rw-r--r--actionpack/test/template/prototype_helper_test.rb26
-rw-r--r--actionpack/test/template/render_test.rb72
-rw-r--r--actionpack/test/template/sanitize_helper_test.rb6
-rw-r--r--actionpack/test/template/text_helper_test.rb51
-rw-r--r--actionpack/test/template/translation_helper_test.rb28
-rw-r--r--actionpack/test/template/url_helper_test.rb22
-rwxr-xr-x[-rw-r--r--]activemodel/Rakefile0
-rw-r--r--activerecord/CHANGELOG6
-rw-r--r--[-rwxr-xr-x]activerecord/README0
-rw-r--r--[-rwxr-xr-x]activerecord/Rakefile2
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record.rb23
-rw-r--r--activerecord/lib/active_record/association_preload.rb12
-rwxr-xr-xactiverecord/lib/active_record/associations.rb444
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb2
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb6
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/associations/belongs_to_association.rb0
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb0
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb7
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb2
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/associations/has_one_association.rb4
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/base.rb257
-rw-r--r--activerecord/lib/active_record/calculations.rb2
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/callbacks.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb10
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/connection_adapters/abstract_adapter.rb20
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/connection_adapters/mysql_adapter.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb23
-rw-r--r--activerecord/lib/active_record/dirty.rb16
-rw-r--r--activerecord/lib/active_record/dynamic_finder_match.rb40
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/fixtures.rb8
-rw-r--r--activerecord/lib/active_record/locale/en-US.yml33
-rw-r--r--activerecord/lib/active_record/migration.rb95
-rw-r--r--activerecord/lib/active_record/named_scope.rb14
-rw-r--r--activerecord/lib/active_record/observer.rb12
-rw-r--r--activerecord/lib/active_record/reflection.rb2
-rw-r--r--activerecord/lib/active_record/test_case.rb2
-rw-r--r--activerecord/lib/active_record/transactions.rb42
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/validations.rb275
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/associations/belongs_to_associations_test.rb0
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb14
-rw-r--r--activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb36
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb42
-rw-r--r--activerecord/test/cases/associations/eager_test.rb21
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb26
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb32
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb9
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/associations/has_one_associations_test.rb10
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb40
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb7
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/associations_test.rb4
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/attribute_methods_test.rb0
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/base_test.rb45
-rw-r--r--activerecord/test/cases/column_definition_test.rb8
-rw-r--r--activerecord/test/cases/defaults_test.rb2
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/deprecated_finder_test.rb0
-rw-r--r--activerecord/test/cases/dirty_test.rb12
-rw-r--r--activerecord/test/cases/finder_test.rb56
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/fixtures_test.rb6
-rw-r--r--activerecord/test/cases/helper.rb1
-rw-r--r--activerecord/test/cases/i18n_test.rb46
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/inheritance_test.rb0
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/lifecycle_test.rb40
-rw-r--r--activerecord/test/cases/method_scoping_test.rb2
-rw-r--r--activerecord/test/cases/migration_test.rb77
-rw-r--r--activerecord/test/cases/multiple_db_test.rb25
-rw-r--r--activerecord/test/cases/named_scope_test.rb43
-rw-r--r--activerecord/test/cases/query_cache_test.rb7
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/readonly_test.rb0
-rw-r--r--activerecord/test/cases/reflection_test.rb4
-rw-r--r--activerecord/test/cases/transactions_test.rb62
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/unconnected_test.rb0
-rw-r--r--activerecord/test/cases/validations_i18n_test.rb858
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/validations_test.rb21
-rw-r--r--activerecord/test/connections/native_mysql/connection.rb4
-rw-r--r--activerecord/test/migrations/broken/100_migration_that_raises_exception.rb10
-rw-r--r--activerecord/test/models/author.rb3
-rw-r--r--[-rwxr-xr-x]activerecord/test/models/company.rb7
-rw-r--r--activerecord/test/models/developer.rb2
-rw-r--r--activerecord/test/models/project.rb2
-rw-r--r--[-rwxr-xr-x]activerecord/test/models/reply.rb0
-rw-r--r--[-rwxr-xr-x]activerecord/test/models/topic.rb0
-rw-r--r--activerecord/test/schema/mysql_specific_schema.rb4
-rw-r--r--activerecord/test/schema/schema.rb3
-rw-r--r--activeresource/CHANGELOG4
-rw-r--r--activeresource/README2
-rw-r--r--activeresource/lib/active_resource.rb17
-rw-r--r--activeresource/lib/active_resource/base.rb162
-rw-r--r--activeresource/lib/active_resource/connection.rb18
-rw-r--r--activeresource/lib/active_resource/custom_methods.rb4
-rw-r--r--activeresource/lib/active_resource/http_mock.rb2
-rw-r--r--activeresource/test/abstract_unit.rb22
-rw-r--r--activeresource/test/base_test.rb107
-rw-r--r--activesupport/lib/active_support.rb6
-rw-r--r--activesupport/lib/active_support/basic_object.rb2
-rw-r--r--activesupport/lib/active_support/buffered_logger.rb29
-rw-r--r--activesupport/lib/active_support/cache.rb34
-rw-r--r--activesupport/lib/active_support/cache/compressed_mem_cache_store.rb8
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb8
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb30
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb52
-rw-r--r--activesupport/lib/active_support/callbacks.rb21
-rw-r--r--activesupport/lib/active_support/core_ext/array/access.rb20
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/array/grouping.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/bigdecimal/conversions.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date/behavior.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/duplicable.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/file.rb24
-rw-r--r--activesupport/lib/active_support/core_ext/file/atomic.rb46
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/hash/deep_merge.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/hash/except.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/reverse_merge.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/hash/slice.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/debugger.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/object.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/object/metaclass.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/time/conversions.rb1
-rw-r--r--activesupport/lib/active_support/dependencies.rb268
-rw-r--r--activesupport/lib/active_support/deprecation.rb4
-rw-r--r--activesupport/lib/active_support/inflector.rb70
-rw-r--r--activesupport/lib/active_support/json.rb2
-rw-r--r--activesupport/lib/active_support/json/encoders/date.rb11
-rw-r--r--activesupport/lib/active_support/json/encoders/date_time.rb11
-rw-r--r--activesupport/lib/active_support/json/encoders/time.rb13
-rw-r--r--activesupport/lib/active_support/locale/en-US.yml31
-rw-r--r--activesupport/lib/active_support/memoizable.rb85
-rwxr-xr-x[-rw-r--r--]activesupport/lib/active_support/multibyte/generators/generate_tables.rb0
-rw-r--r--activesupport/lib/active_support/option_merger.rb10
-rw-r--r--activesupport/lib/active_support/testing/performance.rb87
-rw-r--r--activesupport/lib/active_support/testing/setup_and_teardown.rb3
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb14
-rw-r--r--activesupport/lib/active_support/vendor.rb10
-rw-r--r--activesupport/lib/active_support/vendor/builder-2.1.2/builder/xmlevents.rb2
-rwxr-xr-xactivesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb192
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb193
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/exceptions.rb53
-rw-r--r--activesupport/lib/active_support/vendor/memcache-client-1.5.0/memcache.rb4
-rw-r--r--activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/data_timezone.rb2
-rw-r--r--activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/data_timezone_info.rb2
-rw-r--r--activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/linked_timezone.rb2
-rw-r--r--activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/timezone.rb2
-rw-r--r--activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/timezone_transition_info.rb2
-rw-r--r--activesupport/lib/active_support/vendor/xml-simple-1.0.11/xmlsimple.rb6
-rw-r--r--activesupport/test/buffered_logger_test.rb29
-rw-r--r--activesupport/test/caching_test.rb57
-rw-r--r--activesupport/test/callbacks_test.rb8
-rw-r--r--activesupport/test/core_ext/date_ext_test.rb16
-rw-r--r--activesupport/test/core_ext/duplicable_test.rb2
-rw-r--r--activesupport/test/core_ext/file_test.rb50
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb31
-rw-r--r--activesupport/test/core_ext/object_and_class_ext_test.rb8
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb4
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb9
-rw-r--r--activesupport/test/dependencies_test.rb12
-rw-r--r--activesupport/test/i18n_test.rb75
-rw-r--r--activesupport/test/inflector_test.rb11
-rw-r--r--activesupport/test/memoizable_test.rb204
-rw-r--r--activesupport/test/option_merger_test.rb27
-rwxr-xr-xci/ci_build.rb116
-rw-r--r--ci/ci_setup_notes.txt120
-rw-r--r--ci/cruise_config.rb5
-rw-r--r--ci/geminstaller.yml17
-rw-r--r--ci/site.css13
-rw-r--r--ci/site_config.rb72
-rw-r--r--railties/CHANGELOG8
-rw-r--r--railties/Rakefile15
-rwxr-xr-x[-rw-r--r--]railties/bin/about0
-rwxr-xr-x[-rw-r--r--]railties/bin/console0
-rwxr-xr-x[-rw-r--r--]railties/bin/destroy0
-rwxr-xr-x[-rw-r--r--]railties/bin/generate0
-rwxr-xr-x[-rw-r--r--]railties/bin/performance/benchmarker0
-rwxr-xr-x[-rw-r--r--]railties/bin/performance/profiler0
-rwxr-xr-x[-rw-r--r--]railties/bin/performance/request0
-rwxr-xr-x[-rw-r--r--]railties/bin/plugin0
-rwxr-xr-x[-rw-r--r--]railties/bin/process/inspector0
-rwxr-xr-x[-rw-r--r--]railties/bin/process/reaper0
-rwxr-xr-x[-rw-r--r--]railties/bin/process/spawner0
-rwxr-xr-x[-rw-r--r--]railties/bin/runner0
-rwxr-xr-x[-rw-r--r--]railties/bin/server0
-rw-r--r--railties/config.ru17
-rw-r--r--[-rwxr-xr-x]railties/configs/apache.conf0
-rwxr-xr-xrailties/dispatches/dispatch.fcgi2
-rwxr-xr-xrailties/dispatches/dispatch.rb4
-rwxr-xr-x[-rw-r--r--]railties/dispatches/gateway.cgi4
-rw-r--r--railties/doc/guides/actionview/helpers.markdown91
-rw-r--r--railties/doc/guides/actionview/partials.markdown90
-rw-r--r--railties/doc/guides/activerecord/basics.markdown56
-rw-r--r--railties/doc/guides/creating_plugins/acts_as_yaffle.txt193
-rw-r--r--railties/doc/guides/creating_plugins/appendix.txt46
-rw-r--r--railties/doc/guides/creating_plugins/basics.markdown861
-rw-r--r--railties/doc/guides/creating_plugins/creating_plugins.txt84
-rw-r--r--railties/doc/guides/creating_plugins/custom_generator.txt69
-rw-r--r--railties/doc/guides/creating_plugins/custom_route.txt69
-rw-r--r--railties/doc/guides/creating_plugins/migration_generator.txt89
-rw-r--r--railties/doc/guides/creating_plugins/odds_and_ends.txt122
-rw-r--r--railties/doc/guides/creating_plugins/preparation.txt169
-rw-r--r--railties/doc/guides/creating_plugins/string_to_squawk.txt103
-rw-r--r--railties/doc/guides/creating_plugins/view_helper.txt61
-rw-r--r--railties/doc/guides/icons/README5
-rw-r--r--railties/doc/guides/icons/callouts/1.pngbin0 -> 329 bytes
-rw-r--r--railties/doc/guides/icons/callouts/10.pngbin0 -> 361 bytes
-rw-r--r--railties/doc/guides/icons/callouts/11.pngbin0 -> 565 bytes
-rw-r--r--railties/doc/guides/icons/callouts/12.pngbin0 -> 617 bytes
-rw-r--r--railties/doc/guides/icons/callouts/13.pngbin0 -> 623 bytes
-rw-r--r--railties/doc/guides/icons/callouts/14.pngbin0 -> 411 bytes
-rw-r--r--railties/doc/guides/icons/callouts/15.pngbin0 -> 640 bytes
-rw-r--r--railties/doc/guides/icons/callouts/2.pngbin0 -> 353 bytes
-rw-r--r--railties/doc/guides/icons/callouts/3.pngbin0 -> 350 bytes
-rw-r--r--railties/doc/guides/icons/callouts/4.pngbin0 -> 345 bytes
-rw-r--r--railties/doc/guides/icons/callouts/5.pngbin0 -> 348 bytes
-rw-r--r--railties/doc/guides/icons/callouts/6.pngbin0 -> 355 bytes
-rw-r--r--railties/doc/guides/icons/callouts/7.pngbin0 -> 344 bytes
-rw-r--r--railties/doc/guides/icons/callouts/8.pngbin0 -> 357 bytes
-rw-r--r--railties/doc/guides/icons/callouts/9.pngbin0 -> 357 bytes
-rw-r--r--railties/doc/guides/icons/caution.pngbin0 -> 2554 bytes
-rw-r--r--railties/doc/guides/icons/example.pngbin0 -> 2354 bytes
-rw-r--r--railties/doc/guides/icons/home.pngbin0 -> 1340 bytes
-rw-r--r--railties/doc/guides/icons/important.pngbin0 -> 2657 bytes
-rw-r--r--railties/doc/guides/icons/next.pngbin0 -> 1302 bytes
-rw-r--r--railties/doc/guides/icons/note.pngbin0 -> 2730 bytes
-rw-r--r--railties/doc/guides/icons/prev.pngbin0 -> 1348 bytes
-rw-r--r--railties/doc/guides/icons/tip.pngbin0 -> 2602 bytes
-rw-r--r--railties/doc/guides/icons/up.pngbin0 -> 1320 bytes
-rw-r--r--railties/doc/guides/icons/warning.pngbin0 -> 2828 bytes
-rw-r--r--railties/doc/guides/securing_rails_applications/creating_records_directly_from_form_parameters.txt86
-rw-r--r--railties/doc/guides/securing_rails_applications/cross_site_scripting.txt64
-rw-r--r--railties/doc/guides/securing_rails_applications/securing_rails_applications.txt14
-rw-r--r--railties/doc/guides/securing_rails_applications/sql_injection.txt87
-rw-r--r--railties/doc/guides/testing_rails_applications/testing_rails_applications.txt1371
-rw-r--r--railties/environments/boot.rb8
-rw-r--r--railties/environments/production.rb4
-rw-r--r--railties/helpers/performance_test_helper.rb7
-rw-r--r--railties/html/500.html5
-rw-r--r--railties/html/javascripts/prototype.js304
-rw-r--r--railties/lib/commands/dbconsole.rb4
-rwxr-xr-x[-rw-r--r--]railties/lib/commands/ncgi/listener4
-rwxr-xr-x[-rw-r--r--]railties/lib/commands/ncgi/tracker4
-rw-r--r--railties/lib/commands/plugin.rb2
-rw-r--r--railties/lib/commands/process/spawner.rb4
-rw-r--r--railties/lib/commands/runner.rb2
-rw-r--r--railties/lib/commands/server.rb6
-rw-r--r--railties/lib/fcgi_handler.rb4
-rw-r--r--railties/lib/initializer.rb129
-rw-r--r--railties/lib/performance_test_help.rb5
-rw-r--r--railties/lib/rails/gem_dependency.rb2
-rw-r--r--railties/lib/rails/rack.rb1
-rw-r--r--railties/lib/rails/rack/logger.rb28
-rw-r--r--railties/lib/rails_generator/commands.rb17
-rw-r--r--railties/lib/rails_generator/generated_attribute.rb4
-rw-r--r--railties/lib/rails_generator/generators/components/model/templates/model.rb3
-rw-r--r--[-rwxr-xr-x]railties/lib/rails_generator/generators/components/plugin/templates/Rakefile0
-rw-r--r--railties/lib/rails_generator/generators/components/scaffold/USAGE14
-rw-r--r--railties/lib/rails_generator/scripts.rb2
-rw-r--r--railties/lib/rails_generator/scripts/destroy.rb13
-rw-r--r--railties/lib/rails_generator/secret_key_generator.rb2
-rw-r--r--railties/lib/tasks/databases.rake9
-rw-r--r--railties/lib/tasks/documentation.rake1
-rw-r--r--railties/lib/tasks/framework.rake5
-rw-r--r--railties/test/error_page_test.rb43
-rw-r--r--railties/test/fixtures/eager/zoo.rb3
-rw-r--r--railties/test/fixtures/eager/zoo/reptile_house.rb2
-rw-r--r--railties/test/gem_dependency_test.rb9
-rw-r--r--railties/test/generators/generator_test_helper.rb5
-rw-r--r--railties/test/generators/rails_model_generator_test.rb16
-rw-r--r--railties/test/initializer_test.rb39
408 files changed, 15564 insertions, 5834 deletions
diff --git a/.gitignore b/.gitignore
index bba7d5dce8..a86055e841 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,5 @@ actionpack/pkg
actionmailer/pkg
activesupport/pkg
railties/pkg
+railties/test/500.html
*.rbc
diff --git a/actionmailer/README b/actionmailer/README
index 0e16ea6ec6..0e16ea6ec6 100755..100644
--- a/actionmailer/README
+++ b/actionmailer/README
diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile
index 612bd27774..612bd27774 100755..100644
--- a/actionmailer/Rakefile
+++ b/actionmailer/Rakefile
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
index 2e324d4637..2a9210deb9 100755..100644
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -21,13 +21,13 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-unless defined?(ActionController)
- begin
- $:.unshift "#{File.dirname(__FILE__)}/../../actionpack/lib"
+begin
+ require 'action_controller'
+rescue LoadError
+ actionpack_path = "#{File.dirname(__FILE__)}/../../actionpack/lib"
+ if File.directory?(actionpack_path)
+ $:.unshift actionpack_path
require 'action_controller'
- rescue LoadError
- require 'rubygems'
- gem 'actionpack', '>= 1.12.5'
end
end
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index 5a71935009..72c94529b5 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -216,7 +216,7 @@ module ActionMailer #:nodoc:
# * <tt>:domain</tt> - If you need to specify a HELO domain, you can do it here.
# * <tt>:user_name</tt> - If your mail server requires authentication, set the username in this setting.
# * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting.
- # * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the authentication type here.
+ # * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the authentication type here.
# This is a symbol and one of <tt>:plain</tt>, <tt>:login</tt>, <tt>:cram_md5</tt>.
#
# * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
@@ -233,10 +233,10 @@ module ActionMailer #:nodoc:
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with <tt>delivery_method :test</tt>. Most useful
# for unit and functional testing.
#
- # * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also
+ # * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also
# pick a different charset from inside a method with +charset+.
# * <tt>default_content_type</tt> - The default content type used for the main part of the message. Defaults to "text/plain". You
- # can also pick a different content type from inside a method with +content_type+.
+ # can also pick a different content type from inside a method with +content_type+.
# * <tt>default_mime_version</tt> - The default mime version used for the message. Defaults to <tt>1.0</tt>. You
# can also pick a different value from inside a method with +mime_version+.
# * <tt>default_implicit_parts_order</tt> - When a message is built implicitly (i.e. multiple parts are assembled from templates
@@ -250,12 +250,9 @@ module ActionMailer #:nodoc:
private_class_method :new #:nodoc:
- class_inheritable_accessor :template_root
+ class_inheritable_accessor :view_paths
cattr_accessor :logger
- cattr_accessor :template_extensions
- @@template_extensions = ['erb', 'builder', 'rhtml', 'rxml']
-
@@smtp_settings = {
:address => "localhost",
:port => 25,
@@ -414,19 +411,18 @@ module ActionMailer #:nodoc:
new.deliver!(mail)
end
- # Register a template extension so mailer templates written in a
- # templating language other than rhtml or rxml are supported.
- # To use this, include in your template-language plugin's init
- # code or on a per-application basis, this can be invoked from
- # <tt>config/environment.rb</tt>:
- #
- # ActionMailer::Base.register_template_extension('haml')
def register_template_extension(extension)
- template_extensions << extension
+ ActiveSupport::Deprecation.warn(
+ "ActionMailer::Base.register_template_extension has been deprecated." +
+ "Use ActionView::Base.register_template_extension instead", caller)
+ end
+
+ def template_root
+ self.view_paths && self.view_paths.first
end
def template_root=(root)
- write_inheritable_attribute(:template_root, ActionView::PathSet.new(Array(root)))
+ self.view_paths = ActionView::Base.process_view_paths(root)
end
end
@@ -451,16 +447,18 @@ module ActionMailer #:nodoc:
# "the_template_file.text.html.erb", etc.). Only do this if parts
# have not already been specified manually.
if @parts.empty?
- templates = Dir.glob("#{template_path}/#{@template}.*")
- templates.each do |path|
- basename = File.basename(path)
- template_regex = Regexp.new("^([^\\\.]+)\\\.([^\\\.]+\\\.[^\\\.]+)\\\.(" + template_extensions.join('|') + ")$")
- next unless md = template_regex.match(basename)
- template_name = basename
- content_type = md.captures[1].gsub('.', '/')
- @parts << Part.new(:content_type => content_type,
- :disposition => "inline", :charset => charset,
- :body => render_message(template_name, @body))
+ Dir.glob("#{template_path}/#{@template}.*").each do |path|
+ template = template_root["#{mailer_name}/#{File.basename(path)}"]
+
+ # Skip unless template has a multipart format
+ next unless template.multipart?
+
+ @parts << Part.new(
+ :content_type => template.content_type,
+ :disposition => "inline",
+ :charset => charset,
+ :body => render_message(template, @body)
+ )
end
unless @parts.empty?
@content_type = "multipart/alternative"
@@ -473,7 +471,7 @@ module ActionMailer #:nodoc:
# normal template exists (or if there were no implicit parts) we render
# it.
template_exists = @parts.empty?
- template_exists ||= Dir.glob("#{template_path}/#{@template}.*").any? { |i| File.basename(i).split(".").length == 2 }
+ template_exists ||= template_root["#{mailer_name}/#{@template}"]
@body = render_message(@template, @body) if template_exists
# Finally, if there are other message parts and a textual body exists,
@@ -529,24 +527,31 @@ module ActionMailer #:nodoc:
end
def render_message(method_name, body)
- render :file => method_name, :body => body, :use_full_path => true
+ render :file => method_name, :body => body
end
def render(opts)
body = opts.delete(:body)
- if opts[:file] && opts[:file] !~ /\//
+ if opts[:file] && (opts[:file] !~ /\// && !opts[:file].respond_to?(:render))
opts[:file] = "#{mailer_name}/#{opts[:file]}"
end
- opts[:use_full_path] = true
initialize_template_class(body).render(opts)
end
+ def template_root
+ self.class.template_root
+ end
+
+ def template_root=(root)
+ self.class.template_root = root
+ end
+
def template_path
"#{template_root}/#{mailer_name}"
end
def initialize_template_class(assigns)
- ActionView::Base.new(template_root, assigns, self)
+ ActionView::Base.new(view_paths, assigns, self)
end
def sort_parts(parts, order = [])
diff --git a/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/address.rb b/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/address.rb
index fa8e5bcd8c..982ad5b661 100644
--- a/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/address.rb
+++ b/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/address.rb
@@ -38,7 +38,7 @@ module TMail
# = Class Address
#
# Provides a complete handling library for email addresses. Can parse a string of an
- # address directly or take in preformatted addresses themseleves. Allows you to add
+ # address directly or take in preformatted addresses themselves. Allows you to add
# and remove phrases from the front of the address and provides a compare function for
# email addresses.
#
@@ -143,7 +143,7 @@ module TMail
# This is to catch an unquoted "@" symbol in the local part of the
# address. Handles addresses like <"@"@me.com> and makes sure they
- # stay like <"@"@me.com> (previously were becomming <@@me.com>)
+ # stay like <"@"@me.com> (previously were becoming <@@me.com>)
if local && (local.join == '@' || local.join =~ /\A[^"].*?@.*?[^"]\Z/)
@local = "\"#{local.join}\""
else
diff --git a/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/header.rb b/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/header.rb
index 9153dcd7c6..dbdefcf979 100644
--- a/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/header.rb
+++ b/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/header.rb
@@ -59,7 +59,7 @@ module TMail
#
# This is because a mailbox doesn't have the : after the From that designates the
# beginning of the envelope sender (which can be different to the from address of
- # the emial)
+ # the email)
#
# Other fields can be passed as normal, "Reply-To", "Received" etc.
#
diff --git a/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/interface.rb b/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/interface.rb
index a6d428d7d6..2fc2dbdfc7 100644
--- a/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/interface.rb
+++ b/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/interface.rb
@@ -42,7 +42,7 @@ module TMail
# Allows you to query the mail object with a string to get the contents
# of the field you want.
#
- # Returns a string of the exact contnts of the field
+ # Returns a string of the exact contents of the field
#
# mail.from = "mikel <mikel@lindsaar.net>"
# mail.header_string("From") #=> "mikel <mikel@lindsaar.net>"
diff --git a/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/mail.rb b/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/mail.rb
index 5a319907ae..c3a8803dc4 100644
--- a/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/mail.rb
+++ b/actionmailer/lib/action_mailer/vendor/tmail-1.2.3/tmail/mail.rb
@@ -255,7 +255,7 @@ module TMail
alias fetch []
# Allows you to set or delete TMail header objects at will.
- # Eamples:
+ # Examples:
# @mail = TMail::Mail.new
# @mail['to'].to_s # => 'mikel@test.com.au'
# @mail['to'] = 'mikel@elsewhere.org'
@@ -265,7 +265,7 @@ module TMail
# @mail['to'].to_s # => nil
# @mail.encoded # => "\r\n"
#
- # Note: setting mail[] = nil actualy deletes the header field in question from the object,
+ # Note: setting mail[] = nil actually deletes the header field in question from the object,
# it does not just set the value of the hash to nil
def []=( key, val )
dkey = key.downcase
diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb
index 11058a770d..107b2e8bbe 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -30,12 +30,20 @@ class Net::SMTP
end
end
-# Wrap tests that use Mocha and skip if unavailable.
-def uses_mocha(test_name)
- gem 'mocha', ">=0.9.0"
+def uses_gem(gem_name, test_name, version = '> 0')
+ require 'rubygems'
+ gem gem_name.to_s, version
+ require gem_name.to_s
yield
-rescue Gem::LoadError
- $stderr.puts "Skipping #{test_name} tests (Mocha >= 0.5 is required). `gem install mocha` and try again."
+rescue LoadError
+ $stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again."
+end
+
+# Wrap tests that use Mocha and skip if unavailable.
+unless defined? uses_mocha
+ def uses_mocha(test_name, &block)
+ uses_gem('mocha', test_name, '>= 0.5.5', &block)
+ end
end
def set_delivery_method(delivery_method)
diff --git a/actionmailer/test/fixtures/test_mailer/signed_up.erb b/actionmailer/test/fixtures/test_mailer/signed_up.html.erb
index a85d5fa442..a85d5fa442 100644
--- a/actionmailer/test/fixtures/test_mailer/signed_up.erb
+++ b/actionmailer/test/fixtures/test_mailer/signed_up.html.erb
diff --git a/actionmailer/test/mail_render_test.rb b/actionmailer/test/mail_render_test.rb
index fbcd1887e4..45811612eb 100644
--- a/actionmailer/test/mail_render_test.rb
+++ b/actionmailer/test/mail_render_test.rb
@@ -20,13 +20,13 @@ class RenderMailer < ActionMailer::Base
subject "rendering rxml template"
from "tester@example.com"
end
-
+
def included_subtemplate(recipient)
recipients recipient
subject "Including another template in the one being rendered"
from "tester@example.com"
end
-
+
def included_old_subtemplate(recipient)
recipients recipient
subject "Including another template in the one being rendered"
@@ -83,17 +83,11 @@ class RenderHelperTest < Test::Unit::TestCase
mail = RenderMailer.deliver_rxml_template(@recipient)
assert_equal "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<test/>", mail.body.strip
end
-
+
def test_included_subtemplate
mail = RenderMailer.deliver_included_subtemplate(@recipient)
assert_equal "Hey Ho, let's go!", mail.body.strip
end
-
- def test_deprecated_old_subtemplate
- assert_raises ActionView::ActionViewError do
- RenderMailer.deliver_included_old_subtemplate(@recipient)
- end
- end
end
class FirstSecondHelperTest < Test::Unit::TestCase
diff --git a/actionmailer/test/mail_service_test.rb b/actionmailer/test/mail_service_test.rb
index 7f4a8817ca..882b07d675 100755..100644
--- a/actionmailer/test/mail_service_test.rb
+++ b/actionmailer/test/mail_service_test.rb
@@ -219,7 +219,7 @@ class TestMailer < ActionMailer::Base
end
attachment :content_type => "application/octet-stream",:filename => "test.txt", :body => "test abcdefghijklmnopqstuvwxyz"
end
-
+
def nested_multipart_with_body(recipient)
recipients recipient
subject "nested multipart with body"
@@ -321,7 +321,7 @@ class ActionMailerTest < Test::Unit::TestCase
assert_nothing_raised { created = TestMailer.create_nested_multipart(@recipient)}
assert_equal 2,created.parts.size
assert_equal 2,created.parts.first.parts.size
-
+
assert_equal "multipart/mixed", created.content_type
assert_equal "multipart/alternative", created.parts.first.content_type
assert_equal "bar", created.parts.first.header['foo'].to_s
@@ -366,7 +366,7 @@ class ActionMailerTest < Test::Unit::TestCase
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
-
+
def test_custom_template
expected = new_mail
expected.to = @recipient
@@ -382,7 +382,6 @@ class ActionMailerTest < Test::Unit::TestCase
end
def test_custom_templating_extension
- #
# N.b., custom_templating_extension.text.plain.haml is expected to be in fixtures/test_mailer directory
expected = new_mail
expected.to = @recipient
@@ -390,18 +389,10 @@ class ActionMailerTest < Test::Unit::TestCase
expected.body = "Hello there, \n\nMr. #{@recipient}"
expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12)
-
+
# Stub the render method so no alternative renderers need be present.
ActionView::Base.any_instance.stubs(:render).returns("Hello there, \n\nMr. #{@recipient}")
-
- # If the template is not registered, there should be no parts.
- created = nil
- assert_nothing_raised { created = TestMailer.create_custom_templating_extension(@recipient) }
- assert_not_nil created
- assert_equal 0, created.parts.length
-
- ActionMailer::Base.register_template_extension('haml')
-
+
# Now that the template is registered, there should be one part. The text/plain part.
created = nil
assert_nothing_raised { created = TestMailer.create_custom_templating_extension(@recipient) }
@@ -428,7 +419,7 @@ class ActionMailerTest < Test::Unit::TestCase
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
-
+
def test_cc_bcc
expected = new_mail
expected.to = @recipient
@@ -550,7 +541,7 @@ class ActionMailerTest < Test::Unit::TestCase
TestMailer.deliver_signed_up(@recipient)
assert_equal 1, ActionMailer::Base.deliveries.size
end
-
+
def test_doesnt_raise_errors_when_raise_delivery_errors_is_false
ActionMailer::Base.raise_delivery_errors = false
TestMailer.any_instance.expects(:perform_delivery_test).raises(Exception)
@@ -670,7 +661,7 @@ EOF
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
-
+
def test_utf8_body_is_not_quoted
@recipient = "Foo áëô îü <extended@example.net>"
expected = new_mail "utf-8"
@@ -760,7 +751,7 @@ EOF
mail = TestMailer.create_multipart_with_mime_version(@recipient)
assert_equal "1.1", mail.mime_version
end
-
+
def test_multipart_with_utf8_subject
mail = TestMailer.create_multipart_with_utf8_subject(@recipient)
assert_match(/\nSubject: =\?utf-8\?Q\?Foo_.*?\?=/, mail.encoded)
@@ -825,7 +816,7 @@ EOF
mail = TestMailer.create_implicitly_multipart_example(@recipient, 'iso-8859-1')
assert_equal "multipart/alternative", mail.header['content-type'].body
-
+
assert_equal 'iso-8859-1', mail.parts[0].sub_header("content-type", "charset")
assert_equal 'iso-8859-1', mail.parts[1].sub_header("content-type", "charset")
assert_equal 'iso-8859-1', mail.parts[2].sub_header("content-type", "charset")
@@ -852,7 +843,7 @@ EOF
assert_equal "line #1\nline #2\nline #3\nline #4\n\n", mail.parts[0].body
assert_equal "<p>line #1</p>\n<p>line #2</p>\n<p>line #3</p>\n<p>line #4</p>\n\n", mail.parts[1].body
end
-
+
def test_headers_removed_on_smtp_delivery
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_cc_bcc(@recipient)
@@ -942,13 +933,13 @@ end # uses_mocha
class InheritableTemplateRootTest < Test::Unit::TestCase
def test_attr
expected = "#{File.dirname(__FILE__)}/fixtures/path.with.dots"
- assert_equal [expected], FunkyPathMailer.template_root.map(&:to_s)
+ assert_equal expected, FunkyPathMailer.template_root
sub = Class.new(FunkyPathMailer)
sub.template_root = 'test/path'
- assert_equal ['test/path'], sub.template_root.map(&:to_s)
- assert_equal [expected], FunkyPathMailer.template_root.map(&:to_s)
+ assert_equal 'test/path', sub.template_root
+ assert_equal expected, FunkyPathMailer.template_root
end
end
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
index 5b7bfe9c30..be490093ac 100644
--- a/actionpack/CHANGELOG
+++ b/actionpack/CHANGELOG
@@ -1,5 +1,35 @@
*Edge*
+* Allow polymorphic_url helper to take url options. #880 [Tarmo Tänav]
+
+* Switched integration test runner to use Rack processor instead of CGI [Josh Peek]
+
+* Made AbstractRequest.if_modified_sense return nil if the header could not be parsed [Jamis Buck]
+
+* Added back ActionController::Base.allow_concurrency flag [Josh Peek]
+
+* AbstractRequest.relative_url_root is no longer automatically configured by a HTTP header. It can now be set in your configuration environment with config.action_controller.relative_url_root [Josh Peek]
+
+* Update Prototype to 1.6.0.2 #599 [Patrick Joyce]
+
+* Conditional GET utility methods. [Jeremy Kemper]
+ response.last_modified = @post.updated_at
+ response.etag = [:admin, @post, current_user]
+
+ if request.fresh?(response)
+ head :not_modified
+ else
+ # render ...
+ end
+
+* All 2xx requests are considered successful [Josh Peek]
+
+* Fixed that AssetTagHelper#compute_public_path shouldn't cache the asset_host along with the source or per-request proc's won't run [DHH]
+
+* Removed config.action_view.cache_template_loading, use config.cache_classes instead [Josh Peek]
+
+* Get buffer for fragment cache from template's @output_buffer [Josh Peek]
+
* Set config.action_view.warn_cache_misses = true to receive a warning if you perform an action that results in an expensive disk operation that could be cached [Josh Peek]
* Refactor template preloading. New abstractions include Renderable mixins and a refactored Template class [Josh Peek]
diff --git a/actionpack/README b/actionpack/README
index 2746c3cc43..6090089bb9 100644
--- a/actionpack/README
+++ b/actionpack/README
@@ -31,7 +31,7 @@ http://www.rubyonrails.org.
A short rundown of the major features:
* Actions grouped in controller as methods instead of separate command objects
- and can therefore share helper methods.
+ and can therefore share helper methods
BlogController < ActionController::Base
def show
@@ -168,7 +168,7 @@ A short rundown of the major features:
{Learn more}[link:classes/ActionController/Base.html]
-* Javascript and Ajax integration.
+* Javascript and Ajax integration
link_to_function "Greeting", "alert('Hello world!')"
link_to_remote "Delete this post", :update => "posts",
@@ -177,7 +177,7 @@ A short rundown of the major features:
{Learn more}[link:classes/ActionView/Helpers/JavaScriptHelper.html]
-* Pagination for navigating lists of results.
+* Pagination for navigating lists of results
# controller
def list
@@ -192,15 +192,9 @@ A short rundown of the major features:
{Learn more}[link:classes/ActionController/Pagination.html]
-* Easy testing of both controller and template result through TestRequest/Response
-
- class LoginControllerTest < Test::Unit::TestCase
- def setup
- @controller = LoginController.new
- @request = ActionController::TestRequest.new
- @response = ActionController::TestResponse.new
- end
+* Easy testing of both controller and rendered template through ActionController::TestCase
+ class LoginControllerTest < ActionController::TestCase
def test_failing_authenticate
process :authenticate, :user_name => "nop", :password => ""
assert flash.has_key?(:alert)
@@ -208,7 +202,7 @@ A short rundown of the major features:
end
end
- {Learn more}[link:classes/ActionController/TestRequest.html]
+ {Learn more}[link:classes/ActionController/TestCase.html]
* Automated benchmarking and integrated logging
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index 3c4a339d50..e58071d4af 100755..100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -21,16 +21,13 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-$:.unshift(File.dirname(__FILE__)) unless
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
-
-unless defined?(ActiveSupport)
- begin
- $:.unshift "#{File.dirname(__FILE__)}/../../activesupport/lib"
+begin
+ require 'active_support'
+rescue LoadError
+ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
+ if File.directory?(activesupport_path)
+ $:.unshift activesupport_path
require 'active_support'
- rescue LoadError
- require 'rubygems'
- gem 'activesupport'
end
end
diff --git a/actionpack/lib/action_controller/assertions/response_assertions.rb b/actionpack/lib/action_controller/assertions/response_assertions.rb
index 765225ae24..e2e8bbdc71 100644
--- a/actionpack/lib/action_controller/assertions/response_assertions.rb
+++ b/actionpack/lib/action_controller/assertions/response_assertions.rb
@@ -87,11 +87,11 @@ module ActionController
#
def assert_template(expected = nil, message=nil)
clean_backtrace do
- rendered = @response.rendered_template
+ rendered = @response.rendered_template.to_s
msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered)
assert_block(msg) do
if expected.nil?
- @response.rendered_template.nil?
+ @response.rendered_template.blank?
else
rendered.to_s.match(expected)
end
diff --git a/actionpack/lib/action_controller/assertions/routing_assertions.rb b/actionpack/lib/action_controller/assertions/routing_assertions.rb
index 491b72d586..312b4e228b 100644
--- a/actionpack/lib/action_controller/assertions/routing_assertions.rb
+++ b/actionpack/lib/action_controller/assertions/routing_assertions.rb
@@ -2,7 +2,7 @@ module ActionController
module Assertions
# Suite of assertions to test routes generated by Rails and the handling of requests made to them.
module RoutingAssertions
- # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
+ # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
# match +path+. Basically, it asserts that Rails recognizes the route given by +expected_options+.
#
# Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes
@@ -14,16 +14,16 @@ module ActionController
#
# You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
# to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the
- # extras argument, appending the query string on the path directly will not work. For example:
+ # extras argument, appending the query string on the path directly will not work. For example:
#
# # assert that a path of '/items/list/1?view=print' returns the correct options
- # assert_recognizes({:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" })
+ # assert_recognizes({:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" })
#
- # The +message+ parameter allows you to pass in an error message that is displayed upon failure.
+ # The +message+ parameter allows you to pass in an error message that is displayed upon failure.
#
# ==== Examples
# # Check the default route (i.e., the index action)
- # assert_recognizes({:controller => 'items', :action => 'index'}, 'items')
+ # assert_recognizes({:controller => 'items', :action => 'index'}, 'items')
#
# # Test a specific action
# assert_recognizes({:controller => 'items', :action => 'list'}, 'items/list')
@@ -44,16 +44,16 @@ module ActionController
request_method = nil
end
- clean_backtrace do
- ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
+ clean_backtrace do
+ ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
request = recognized_request_for(path, request_method)
-
+
expected_options = expected_options.clone
extras.each_key { |key| expected_options.delete key } unless extras.nil?
-
+
expected_options.stringify_keys!
routing_diff = expected_options.diff(request.path_parameters)
- msg = build_message(message, "The recognized options <?> did not match <?>, difference: <?>",
+ msg = build_message(message, "The recognized options <?> did not match <?>, difference: <?>",
request.path_parameters, expected_options, expected_options.diff(request.path_parameters))
assert_block(msg) { request.path_parameters == expected_options }
end
@@ -64,7 +64,7 @@ module ActionController
# a query string. The +message+ parameter allows you to specify a custom error message for assertion failures.
#
# The +defaults+ parameter is unused.
- #
+ #
# ==== Examples
# # Asserts that the default action is generated for a route with no action
# assert_generates("/items", :controller => "items", :action => "index")
@@ -73,34 +73,34 @@ module ActionController
# assert_generates("/items/list", :controller => "items", :action => "list")
#
# # Tests the generation of a route with a parameter
- # assert_generates("/items/list/1", { :controller => "items", :action => "list", :id => "1" })
+ # assert_generates("/items/list/1", { :controller => "items", :action => "list", :id => "1" })
#
# # Asserts that the generated route gives us our custom route
# assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" }
def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)
- clean_backtrace do
+ clean_backtrace do
expected_path = "/#{expected_path}" unless expected_path[0] == ?/
# Load routes.rb if it hasn't been loaded.
- ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
-
+ ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
+
generated_path, extra_keys = ActionController::Routing::Routes.generate_extras(options, defaults)
found_extras = options.reject {|k, v| ! extra_keys.include? k}
msg = build_message(message, "found extras <?>, not <?>", found_extras, extras)
assert_block(msg) { found_extras == extras }
-
- msg = build_message(message, "The generated path <?> did not match <?>", generated_path,
+
+ msg = build_message(message, "The generated path <?> did not match <?>", generated_path,
expected_path)
assert_block(msg) { expected_path == generated_path }
end
end
- # Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates
+ # Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates
# <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+
# and +assert_generates+ into one step.
#
# The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The
- # +message+ parameter allows you to specify a custom error message to display upon failure.
+ # +message+ parameter allows you to specify a custom error message to display upon failure.
#
# ==== Examples
# # Assert a basic route: a controller with the default action (index)
@@ -119,12 +119,12 @@ module ActionController
# assert_routing({ :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" })
def assert_routing(path, options, defaults={}, extras={}, message=nil)
assert_recognizes(options, path, extras, message)
-
- controller, default_controller = options[:controller], defaults[:controller]
+
+ controller, default_controller = options[:controller], defaults[:controller]
if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
options[:controller] = "/#{controller}"
end
-
+
assert_generates(path.is_a?(Hash) ? path[:path] : path, options, defaults, extras, message)
end
diff --git a/actionpack/lib/action_controller/assertions/selector_assertions.rb b/actionpack/lib/action_controller/assertions/selector_assertions.rb
index d3594e711c..9114894b1d 100644
--- a/actionpack/lib/action_controller/assertions/selector_assertions.rb
+++ b/actionpack/lib/action_controller/assertions/selector_assertions.rb
@@ -21,10 +21,8 @@ module ActionController
# from the response HTML or elements selected by the enclosing assertion.
#
# In addition to HTML responses, you can make the following assertions:
- # * +assert_select_rjs+ - Assertions on HTML content of RJS update and
- # insertion operations.
- # * +assert_select_encoded+ - Assertions on HTML encoded inside XML,
- # for example for dealing with feed item descriptions.
+ # * +assert_select_rjs+ - Assertions on HTML content of RJS update and insertion operations.
+ # * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions.
# * +assert_select_email+ - Assertions on the HTML body of an e-mail.
#
# Also see HTML::Selector to learn how to use selectors.
@@ -409,6 +407,7 @@ module ActionController
if rjs_type == :insert
arg = args.shift
+ position = arg
insertion = "insert_#{arg}".to_sym
raise ArgumentError, "Unknown RJS insertion type #{arg}" unless RJS_STATEMENTS[insertion]
statement = "(#{RJS_STATEMENTS[insertion]})"
@@ -420,6 +419,7 @@ module ActionController
else
statement = "#{RJS_STATEMENTS[:any]}"
end
+ position ||= Regexp.new(RJS_INSERTIONS.join('|'))
# Next argument we're looking for is the element identifier. If missing, we pick
# any element.
@@ -436,9 +436,14 @@ module ActionController
Regexp.new("\\$\\(\"#{id}\"\\)#{statement}\\(#{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE)
when :remove, :show, :hide, :toggle
Regexp.new("#{statement}\\(\"#{id}\"\\)")
- else
- Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE)
- end
+ when :replace, :replace_html
+ Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)")
+ when :insert, :insert_html
+ Regexp.new("Element.insert\\(\\\"#{id}\\\", \\{ #{position}: #{RJS_PATTERN_HTML} \\}\\);")
+ else
+ Regexp.union(Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)"),
+ Regexp.new("Element.insert\\(\\\"#{id}\\\", \\{ #{position}: #{RJS_PATTERN_HTML} \\}\\);"))
+ end
# Duplicate the body since the next step involves destroying it.
matches = nil
@@ -447,7 +452,7 @@ module ActionController
matches = @response.body.match(pattern)
else
@response.body.gsub(pattern) do |match|
- html = unescape_rjs($2)
+ html = unescape_rjs(match)
matches ||= []
matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? }
""
@@ -587,17 +592,16 @@ module ActionController
:hide => /Element\.hide/,
:toggle => /Element\.toggle/
}
+ RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})")
+ RJS_PATTERN_HTML = /"((\\"|[^"])*)"/
RJS_INSERTIONS = [:top, :bottom, :before, :after]
RJS_INSERTIONS.each do |insertion|
- RJS_STATEMENTS["insert_#{insertion}".to_sym] = Regexp.new(Regexp.quote("new Insertion.#{insertion.to_s.camelize}"))
+ RJS_STATEMENTS["insert_#{insertion}".to_sym] = /Element.insert\(\"([^\"]*)\", \{ #{insertion.to_s.downcase}: #{RJS_PATTERN_HTML} \}\);/
end
- RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})")
RJS_STATEMENTS[:insert_html] = Regexp.new(RJS_INSERTIONS.collect do |insertion|
- Regexp.quote("new Insertion.#{insertion.to_s.camelize}")
+ /Element.insert\(\"([^\"]*)\", \{ #{insertion.to_s.downcase}: #{RJS_PATTERN_HTML} \}\);/
end.join('|'))
- RJS_PATTERN_HTML = /"((\\"|[^"])*)"/
- RJS_PATTERN_EVERYTHING = Regexp.new("#{RJS_STATEMENTS[:any]}\\(\"([^\"]*)\", #{RJS_PATTERN_HTML}\\)",
- Regexp::MULTILINE)
+ RJS_PATTERN_EVERYTHING = Regexp.new("#{RJS_STATEMENTS[:any]}\\(\"([^\"]*)\", #{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE)
RJS_PATTERN_UNICODE_ESCAPED_CHAR = /\\u([0-9a-zA-Z]{4})/
end
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index df94f78f18..91023cd774 100755..100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -283,6 +283,14 @@ module ActionController #:nodoc:
@@debug_routes = true
cattr_accessor :debug_routes
+ # Indicates whether to allow concurrent action processing. Your
+ # controller actions and any other code they call must also behave well
+ # when called from concurrent threads. Turned off by default.
+ @@allow_concurrency = false
+ cattr_accessor :allow_concurrency
+
+ @@guard = Monitor.new
+
# Modern REST web services often need to submit complex data to the web application.
# The <tt>@@param_parsers</tt> hash lets you register handlers which will process the HTTP body and add parameters to the
# <tt>params</tt> hash. These handlers are invoked for POST and PUT requests.
@@ -354,6 +362,15 @@ module ActionController #:nodoc:
class_inheritable_accessor :allow_forgery_protection
self.allow_forgery_protection = true
+ # If you are deploying to a subdirectory, you will need to set
+ # <tt>config.action_controller.relative_url_root</tt>
+ # This defaults to ENV['RAILS_RELATIVE_URL_ROOT']
+ cattr_writer :relative_url_root
+
+ def self.relative_url_root
+ @@relative_url_root || ENV['RAILS_RELATIVE_URL_ROOT']
+ end
+
# Holds the request object that's primarily used to get environment variables through access like
# <tt>request.env["REQUEST_URI"]</tt>.
attr_internal :request
@@ -411,11 +428,7 @@ module ActionController #:nodoc:
# By default, all methods defined in ActionController::Base and included modules are hidden.
# More methods can be hidden using <tt>hide_actions</tt>.
def hidden_actions
- unless read_inheritable_attribute(:hidden_actions)
- write_inheritable_attribute(:hidden_actions, ActionController::Base.public_instance_methods.map { |m| m.to_s })
- end
-
- read_inheritable_attribute(:hidden_actions)
+ read_inheritable_attribute(:hidden_actions) || write_inheritable_attribute(:hidden_actions, [])
end
# Hide each of the given methods from being callable as actions.
@@ -519,6 +532,8 @@ module ActionController #:nodoc:
public
# Extracts the action_name from the request parameters and performs that action.
def process(request, response, method = :perform_action, *arguments) #:nodoc:
+ response.request = request
+
initialize_template_class(response)
assign_shortcuts(request, response)
initialize_current_url
@@ -526,11 +541,13 @@ module ActionController #:nodoc:
forget_variables_added_to_assigns
log_processing
- send(method, *arguments)
- assign_default_content_type_and_charset
+ if @@allow_concurrency
+ send(method, *arguments)
+ else
+ @@guard.synchronize { send(method, *arguments) }
+ end
- response.request = request
response.prepare! unless component_request?
response
ensure
@@ -763,9 +780,6 @@ module ActionController #:nodoc:
# render :file => "/path/to/some/template.erb", :layout => true, :status => 404
# render :file => "c:/path/to/some/template.erb", :layout => true, :status => 404
#
- # # Renders a template relative to the template root and chooses the proper file extension
- # render :file => "some/template", :use_full_path => true
- #
# === Rendering text
#
# Rendering of text is usually used for tests or for rendering prepared content, such as a cache. By default, text
@@ -896,21 +910,10 @@ module ActionController #:nodoc:
response.content_type ||= Mime::JSON
render_for_text(json, options[:status])
- elsif partial = options[:partial]
- partial = default_template_name if partial == true
+ elsif options[:partial]
+ options[:partial] = default_template_name if options[:partial] == true
add_variables_to_assigns
-
- if collection = options[:collection]
- render_for_text(
- @template.send!(:render_partial_collection, partial, collection,
- options[:spacer_template], options[:locals], options[:as]), options[:status]
- )
- else
- render_for_text(
- @template.send!(:render_partial, partial,
- options[:object], options[:locals]), options[:status]
- )
- end
+ render_for_text(@template.render(options), options[:status])
elsif options[:update]
add_variables_to_assigns
@@ -921,8 +924,7 @@ module ActionController #:nodoc:
render_for_text(generator.to_s, options[:status])
elsif options[:nothing]
- # Safari doesn't pass the headers of the return if the response is zero length
- render_for_text(" ", options[:status])
+ render_for_text(nil, options[:status])
else
render_for_file(default_template_name, options[:status], true)
@@ -968,6 +970,17 @@ module ActionController #:nodoc:
render :nothing => true, :status => status
end
+ # Sets the Last-Modified response header. Returns 304 Not Modified if the
+ # If-Modified-Since request header is <= last modified.
+ def last_modified!(utc_time)
+ head(:not_modified) if response.last_modified!(utc_time)
+ end
+
+ # Sets the ETag response header. Returns 304 Not Modified if the
+ # If-None-Match request header matches.
+ def etag!(etag)
+ head(:not_modified) if response.etag!(etag)
+ end
# Clears the rendered results, allowing for another render to be performed.
def erase_render_results #:nodoc:
@@ -1125,7 +1138,11 @@ module ActionController #:nodoc:
response.body ||= ''
response.body << text.to_s
else
- response.body = text.is_a?(Proc) ? text : text.to_s
+ response.body = case text
+ when Proc then text
+ when nil then " " # Safari doesn't pass the headers of the return if the response is zero length
+ else text.to_s
+ end
end
end
@@ -1155,7 +1172,7 @@ module ActionController #:nodoc:
def log_processing
if logger && logger.info?
- logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin}) [#{request.method.to_s.upcase}]"
+ logger.info "\n\nProcessing #{self.class.name}\##{action_name} (for #{request_origin}) [#{request.method.to_s.upcase}]"
logger.info " Session ID: #{@_session.session_id}" if @_session and @_session.respond_to?(:session_id)
logger.info " Parameters: #{respond_to?(:filter_parameters) ? filter_parameters(params).inspect : params.inspect}"
end
@@ -1166,16 +1183,16 @@ module ActionController #:nodoc:
end
def perform_action
- if self.class.action_methods.include?(action_name)
+ if action_methods.include?(action_name)
send(action_name)
default_render unless performed?
elsif respond_to? :method_missing
method_missing action_name
default_render unless performed?
- elsif template_exists? && template_public?
+ elsif template_exists?
default_render
else
- raise UnknownAction, "No action responded to #{action_name}", caller
+ raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller
end
end
@@ -1188,20 +1205,24 @@ module ActionController #:nodoc:
end
def assign_default_content_type_and_charset
- response.content_type ||= Mime::HTML
- response.charset ||= self.class.default_charset unless sending_file?
- end
-
- def sending_file?
- response.headers["Content-Transfer-Encoding"] == "binary"
+ response.assign_default_content_type_and_charset!
end
+ deprecate :assign_default_content_type_and_charset => :'response.assign_default_content_type_and_charset!'
def action_methods
self.class.action_methods
end
def self.action_methods
- @action_methods ||= Set.new(public_instance_methods.map { |m| m.to_s }) - hidden_actions
+ @action_methods ||=
+ # All public instance methods of this class, including ancestors
+ public_instance_methods(true).map { |m| m.to_s }.to_set -
+ # Except for public instance methods of Base and its ancestors
+ Base.public_instance_methods(true).map { |m| m.to_s } +
+ # Be sure to include shadowed public instance methods of this class
+ public_instance_methods(false).map { |m| m.to_s } -
+ # And always exclude explicitly hidden actions
+ hidden_actions
end
def add_variables_to_assigns
@@ -1243,13 +1264,11 @@ module ActionController #:nodoc:
@template.file_exists?(template_name)
end
- def template_public?(template_name = default_template_name)
- @template.file_public?(template_name)
- end
-
def template_exempt_from_layout?(template_name = default_template_name)
template_name = @template.pick_template(template_name).to_s if @template
@@exempt_from_layout.any? { |ext| template_name =~ ext }
+ rescue ActionView::MissingTemplate
+ false
end
def default_template_name(action_name = self.action_name)
diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb
index 57b31ec9d1..e9b434dd25 100644
--- a/actionpack/lib/action_controller/caching/fragments.rb
+++ b/actionpack/lib/action_controller/caching/fragments.rb
@@ -2,7 +2,7 @@ module ActionController #:nodoc:
module Caching
# Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when
# certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple
- # parties. The caching is doing using the cache helper available in the Action View. A template with caching might look something like:
+ # parties. The caching is done using the cache helper available in the Action View. A template with caching might look something like:
#
# <b>Hello <%= @name %></b>
# <% cache do %>
@@ -60,10 +60,8 @@ module ActionController #:nodoc:
ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
end
- def fragment_for(block, name = {}, options = nil) #:nodoc:
+ def fragment_for(buffer, name = {}, options = nil, &block) #:nodoc:
if perform_caching
- buffer = yield
-
if cache = read_fragment(name, options)
buffer.concat(cache)
else
diff --git a/actionpack/lib/action_controller/cgi_process.rb b/actionpack/lib/action_controller/cgi_process.rb
index 8bc5e4c3a7..0ca27b30db 100644
--- a/actionpack/lib/action_controller/cgi_process.rb
+++ b/actionpack/lib/action_controller/cgi_process.rb
@@ -43,7 +43,7 @@ module ActionController #:nodoc:
:session_path => "/", # available to all paths in app
:session_key => "_session_id",
:cookie_only => true
- } unless const_defined?(:DEFAULT_SESSION_OPTIONS)
+ }
def initialize(cgi, session_options = {})
@cgi = cgi
@@ -61,53 +61,14 @@ module ActionController #:nodoc:
end
end
- # The request body is an IO input stream. If the RAW_POST_DATA environment
- # variable is already set, wrap it in a StringIO.
- def body
- if raw_post = env['RAW_POST_DATA']
- raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
- StringIO.new(raw_post)
- else
- @cgi.stdinput
- end
- end
-
- def query_parameters
- @query_parameters ||= self.class.parse_query_parameters(query_string)
- end
-
- def request_parameters
- @request_parameters ||= parse_formatted_request_parameters
+ def body_stream #:nodoc:
+ @cgi.stdinput
end
def cookies
@cgi.cookies.freeze
end
- def host_with_port_without_standard_port_handling
- if forwarded = env["HTTP_X_FORWARDED_HOST"]
- forwarded.split(/,\s?/).last
- elsif http_host = env['HTTP_HOST']
- http_host
- elsif server_name = env['SERVER_NAME']
- server_name
- else
- "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
- end
- end
-
- def host
- host_with_port_without_standard_port_handling.sub(/:\d+$/, '')
- end
-
- def port
- if host_with_port_without_standard_port_handling =~ /:(\d+)$/
- $1.to_i
- else
- standard_port
- end
- end
-
def session
unless defined?(@session)
if @session_options == false
diff --git a/actionpack/lib/action_controller/cookies.rb b/actionpack/lib/action_controller/cookies.rb
index a4cddbcea2..0428f2a23d 100644
--- a/actionpack/lib/action_controller/cookies.rb
+++ b/actionpack/lib/action_controller/cookies.rb
@@ -22,6 +22,16 @@ module ActionController #:nodoc:
#
# cookies.delete :user_name
#
+ # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
+ #
+ # cookies[:key] = {
+ # :value => 'a yummy cookie',
+ # :expires => 1.year.from_now,
+ # :domain => 'domain.com'
+ # }
+ #
+ # cookies.delete(:key, :domain => 'domain.com')
+ #
# The option symbols for setting cookies are:
#
# * <tt>:value</tt> - The cookie's value or list of values (as an array).
diff --git a/actionpack/lib/action_controller/dispatcher.rb b/actionpack/lib/action_controller/dispatcher.rb
index 7df987d525..bdae5f9d86 100644
--- a/actionpack/lib/action_controller/dispatcher.rb
+++ b/actionpack/lib/action_controller/dispatcher.rb
@@ -2,8 +2,6 @@ module ActionController
# Dispatches requests to the appropriate controller and takes care of
# reloading the app after each request when Dependencies.load? is true.
class Dispatcher
- @@guard = Mutex.new
-
class << self
def define_dispatcher_callbacks(cache_classes)
unless cache_classes
@@ -26,7 +24,7 @@ module ActionController
to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers }
end
- after_dispatch :flush_logger if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:flush)
+ after_dispatch :flush_logger if Base.logger && Base.logger.respond_to?(:flush)
end
# Backward-compatible class method takes CGI-specific args. Deprecated
@@ -46,7 +44,7 @@ module ActionController
def to_prepare(identifier = nil, &block)
@prepare_dispatch_callbacks ||= ActiveSupport::Callbacks::CallbackChain.new
callback = ActiveSupport::Callbacks::Callback.new(:prepare_dispatch, block, :identifier => identifier)
- @prepare_dispatch_callbacks | callback
+ @prepare_dispatch_callbacks.replace_or_append!(callback)
end
# If the block raises, send status code as a last-ditch response.
@@ -101,15 +99,13 @@ module ActionController
end
def dispatch
- @@guard.synchronize do
- begin
- run_callbacks :before_dispatch
- handle_request
- rescue Exception => exception
- failsafe_rescue exception
- ensure
- run_callbacks :after_dispatch, :enumerator => :reverse_each
- end
+ begin
+ run_callbacks :before_dispatch
+ handle_request
+ rescue Exception => exception
+ failsafe_rescue exception
+ ensure
+ run_callbacks :after_dispatch, :enumerator => :reverse_each
end
end
@@ -146,7 +142,7 @@ module ActionController
end
def flush_logger
- RAILS_DEFAULT_LOGGER.flush
+ Base.logger.flush
end
protected
diff --git a/actionpack/lib/action_controller/filters.rb b/actionpack/lib/action_controller/filters.rb
index 10dc0cc45b..1d7236f18a 100644
--- a/actionpack/lib/action_controller/filters.rb
+++ b/actionpack/lib/action_controller/filters.rb
@@ -109,16 +109,17 @@ module ActionController #:nodoc:
update_options! options
end
+ # override these to return true in appropriate subclass
def before?
- self.class == BeforeFilter
+ false
end
def after?
- self.class == AfterFilter
+ false
end
def around?
- self.class == AroundFilter
+ false
end
# Make sets of strings from :only/:except options
@@ -170,6 +171,10 @@ module ActionController #:nodoc:
:around
end
+ def around?
+ true
+ end
+
def call(controller, &block)
if should_run_callback?(controller)
method = filter_responds_to_before_and_after? ? around_proc : self.method
@@ -212,6 +217,10 @@ module ActionController #:nodoc:
:before
end
+ def before?
+ true
+ end
+
def call(controller, &block)
super
if controller.send!(:performed?)
@@ -224,6 +233,10 @@ module ActionController #:nodoc:
def type
:after
end
+
+ def after?
+ true
+ end
end
# Filters enable controllers to run shared pre- and post-processing code for its actions. These filters can be used to do
diff --git a/actionpack/lib/action_controller/headers.rb b/actionpack/lib/action_controller/headers.rb
index 7239438c49..139669c66f 100644
--- a/actionpack/lib/action_controller/headers.rb
+++ b/actionpack/lib/action_controller/headers.rb
@@ -1,31 +1,33 @@
+require 'active_support/memoizable'
+
module ActionController
module Http
class Headers < ::Hash
-
- def initialize(constructor = {})
- if constructor.is_a?(Hash)
+ extend ActiveSupport::Memoizable
+
+ def initialize(*args)
+ if args.size == 1 && args[0].is_a?(Hash)
super()
- update(constructor)
+ update(args[0])
else
- super(constructor)
+ super
end
end
-
+
def [](header_name)
if include?(header_name)
- super
+ super
else
- super(normalize_header(header_name))
+ super(env_name(header_name))
end
end
-
-
+
private
- # Takes an HTTP header name and returns it in the
- # format
- def normalize_header(header_name)
+ # Converts a HTTP header name to an environment variable name.
+ def env_name(header_name)
"HTTP_#{header_name.upcase.gsub(/-/, '_')}"
end
+ memoize :env_name
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_controller/integration.rb b/actionpack/lib/action_controller/integration.rb
index 18c2df8b37..198a22e8dc 100644
--- a/actionpack/lib/action_controller/integration.rb
+++ b/actionpack/lib/action_controller/integration.rb
@@ -101,7 +101,7 @@ module ActionController
@https = flag
end
- # Return +true+ if the session is mimicing a secure HTTPS request.
+ # Return +true+ if the session is mimicking a secure HTTPS request.
#
# if session.https?
# ...
@@ -165,11 +165,19 @@ module ActionController
status/100 == 3
end
- # Performs a GET request with the given parameters. The parameters may
- # be +nil+, a Hash, or a string that is appropriately encoded
- # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
- # The headers should be a hash. The keys will automatically be upcased, with the
- # prefix 'HTTP_' added if needed.
+ # Performs a GET request with the given parameters.
+ #
+ # - +path+: The URI (as a String) on which you want to perform a GET request.
+ # - +parameters+: The HTTP parameters that you want to pass. This may be +nil+,
+ # a Hash, or a String that is appropriately encoded
+ # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
+ # - +headers+: Additional HTTP headers to pass, as a Hash. The keys will
+ # automatically be upcased, with the prefix 'HTTP_' added if needed.
+ #
+ # This method returns an AbstractResponse object, which one can use to inspect
+ # the details of the response. Furthermore, if this method was called from an
+ # ActionController::IntegrationTest object, then that object's <tt>@response</tt>
+ # instance variable will point to the same response object.
#
# You can also perform POST, PUT, DELETE, and HEAD requests with +post+,
# +put+, +delete+, and +head+.
@@ -220,21 +228,6 @@ module ActionController
end
private
- class StubCGI < CGI #:nodoc:
- attr_accessor :stdinput, :stdoutput, :env_table
-
- def initialize(env, stdinput = nil)
- self.env_table = env
- self.stdoutput = StringIO.new
-
- super
-
- stdinput.set_encoding(Encoding::BINARY) if stdinput.respond_to?(:set_encoding)
- stdinput.force_encoding(Encoding::BINARY) if stdinput.respond_to?(:force_encoding)
- @stdinput = stdinput.is_a?(IO) ? stdinput : StringIO.new(stdinput || '')
- end
- end
-
# Tailors the session based on the given URI, setting the HTTPS value
# and the hostname.
def interpret_uri(path)
@@ -282,9 +275,8 @@ module ActionController
ActionController::Base.clear_last_instantiation!
- cgi = StubCGI.new(env, data)
- ActionController::Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, cgi.stdoutput)
- @result = cgi.stdoutput.string
+ env['rack.input'] = data.is_a?(IO) ? data : StringIO.new(data || '')
+ @status, @headers, result_body = ActionController::Dispatcher.new.call(env)
@request_count += 1
@controller = ActionController::Base.last_instantiation
@@ -298,32 +290,34 @@ module ActionController
@html_document = nil
- parse_result
- return status
- rescue MultiPartNeededException
- boundary = "----------XnJLe9ZIbbGUYtzPQJ16u1"
- status = process(method, path, multipart_body(parameters, boundary), (headers || {}).merge({"CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}"}))
- return status
- end
+ # Inject status back in for backwords compatibility with CGI
+ @headers['Status'] = @status
- # Parses the result of the response and extracts the various values,
- # like cookies, status, headers, etc.
- def parse_result
- response_headers, result_body = @result.split(/\r\n\r\n/, 2)
+ @status, @status_message = @status.split(/ /)
+ @status = @status.to_i
- @headers = Hash.new { |h,k| h[k] = [] }
- response_headers.to_s.each_line do |line|
- key, value = line.strip.split(/:\s*/, 2)
- @headers[key.downcase] << value
+ cgi_headers = Hash.new { |h,k| h[k] = [] }
+ @headers.each do |key, value|
+ cgi_headers[key.downcase] << value
end
+ cgi_headers['set-cookie'] = cgi_headers['set-cookie'].first
+ @headers = cgi_headers
- (@headers['set-cookie'] || [] ).each do |string|
- name, value = string.match(/^([^=]*)=([^;]*);/)[1,2]
+ @response.headers['cookie'] ||= []
+ (@headers['set-cookie'] || []).each do |cookie|
+ name, value = cookie.match(/^([^=]*)=([^;]*);/)[1,2]
@cookies[name] = value
+
+ # Fake CGI cookie header
+ # DEPRECATE: Use response.headers["Set-Cookie"] instead
+ @response.headers['cookie'] << CGI::Cookie::new("name" => name, "value" => value)
end
- @status, @status_message = @headers["status"].first.to_s.split(/ /)
- @status = @status.to_i
+ return status
+ rescue MultiPartNeededException
+ boundary = "----------XnJLe9ZIbbGUYtzPQJ16u1"
+ status = process(method, path, multipart_body(parameters, boundary), (headers || {}).merge({"CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}"}))
+ return status
end
# Encode the cookies hash in a format suitable for passing to a
@@ -336,13 +330,15 @@ module ActionController
# Get a temporary URL writer object
def generic_url_rewriter
- cgi = StubCGI.new('REQUEST_METHOD' => "GET",
- 'QUERY_STRING' => "",
- "REQUEST_URI" => "/",
- "HTTP_HOST" => host,
- "SERVER_PORT" => https? ? "443" : "80",
- "HTTPS" => https? ? "on" : "off")
- ActionController::UrlRewriter.new(ActionController::CgiRequest.new(cgi), {})
+ env = {
+ 'REQUEST_METHOD' => "GET",
+ 'QUERY_STRING' => "",
+ "REQUEST_URI" => "/",
+ "HTTP_HOST" => host,
+ "SERVER_PORT" => https? ? "443" : "80",
+ "HTTPS" => https? ? "on" : "off"
+ }
+ ActionController::UrlRewriter.new(ActionController::RackRequest.new(env), {})
end
def name_with_prefix(prefix, name)
@@ -443,7 +439,7 @@ EOF
end
%w(get post put head delete cookies assigns
- xml_http_request get_via_redirect post_via_redirect).each do |method|
+ xml_http_request xhr get_via_redirect post_via_redirect).each do |method|
define_method(method) do |*args|
reset! unless @integration_session
# reset the html_document variable, but only for new get/post calls
@@ -499,7 +495,7 @@ EOF
# Delegate unhandled messages to the current session instance.
def method_missing(sym, *args, &block)
reset! unless @integration_session
- returning @integration_session.send!(sym, *args, &block) do
+ returning @integration_session.__send__(sym, *args, &block) do
copy_session_variables!
end
end
diff --git a/actionpack/lib/action_controller/polymorphic_routes.rb b/actionpack/lib/action_controller/polymorphic_routes.rb
index 7c30bf0778..30564c7bb3 100644
--- a/actionpack/lib/action_controller/polymorphic_routes.rb
+++ b/actionpack/lib/action_controller/polymorphic_routes.rb
@@ -102,6 +102,12 @@ module ActionController
args << format if format
named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options)
+
+ url_options = options.except(:action, :routing_type, :format)
+ unless url_options.empty?
+ args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
+ end
+
send!(named_route, *args)
end
@@ -114,19 +120,19 @@ module ActionController
%w(edit new formatted).each do |action|
module_eval <<-EOT, __FILE__, __LINE__
- def #{action}_polymorphic_url(record_or_hash)
- polymorphic_url(record_or_hash, :action => "#{action}")
+ def #{action}_polymorphic_url(record_or_hash, options = {})
+ polymorphic_url(record_or_hash, options.merge(:action => "#{action}"))
end
- def #{action}_polymorphic_path(record_or_hash)
- polymorphic_url(record_or_hash, :action => "#{action}", :routing_type => :path)
+ def #{action}_polymorphic_path(record_or_hash, options = {})
+ polymorphic_url(record_or_hash, options.merge(:action => "#{action}", :routing_type => :path))
end
EOT
end
private
def action_prefix(options)
- options[:action] ? "#{options[:action]}_" : ""
+ options[:action] ? "#{options[:action]}_" : options[:format] ? "formatted_" : ""
end
def routing_type(options)
diff --git a/actionpack/lib/action_controller/rack_process.rb b/actionpack/lib/action_controller/rack_process.rb
index 01bc1ebb26..1ace16da07 100644
--- a/actionpack/lib/action_controller/rack_process.rb
+++ b/actionpack/lib/action_controller/rack_process.rb
@@ -3,7 +3,7 @@ require 'action_controller/session/cookie_store'
module ActionController #:nodoc:
class RackRequest < AbstractRequest #:nodoc:
- attr_accessor :env, :session_options
+ attr_accessor :session_options
attr_reader :cgi
class SessionFixationAttempt < StandardError #:nodoc:
@@ -15,7 +15,7 @@ module ActionController #:nodoc:
:session_path => "/", # available to all paths in app
:session_key => "_session_id",
:cookie_only => true
- } unless const_defined?(:DEFAULT_SESSION_OPTIONS)
+ }
def initialize(env, session_options = DEFAULT_SESSION_OPTIONS)
@session_options = session_options
@@ -24,39 +24,34 @@ module ActionController #:nodoc:
super()
end
- %w[ AUTH_TYPE CONTENT_TYPE GATEWAY_INTERFACE PATH_INFO
- PATH_TRANSLATED QUERY_STRING REMOTE_HOST
+ %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_INFO
+ PATH_TRANSLATED REMOTE_HOST
REMOTE_IDENT REMOTE_USER SCRIPT_NAME
SERVER_NAME SERVER_PROTOCOL
HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
- HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_HOST
+ HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
define_method(env.sub(/^HTTP_/n, '').downcase) do
@env[env]
end
end
- # The request body is an IO input stream. If the RAW_POST_DATA environment
- # variable is already set, wrap it in a StringIO.
- def body
- if raw_post = env['RAW_POST_DATA']
- StringIO.new(raw_post)
+ def query_string
+ qs = super
+ if !qs.blank?
+ qs
else
- @env['rack.input']
+ @env['QUERY_STRING']
end
end
- def key?(key)
- @env.key?(key)
- end
-
- def query_parameters
- @query_parameters ||= self.class.parse_query_parameters(query_string)
+ def body_stream #:nodoc:
+ @env['rack.input']
end
- def request_parameters
- @request_parameters ||= parse_formatted_request_parameters
+ def key?(key)
+ @env.key?(key)
end
def cookies
@@ -70,38 +65,6 @@ module ActionController #:nodoc:
@env["rack.request.cookie_hash"]
end
- def host_with_port_without_standard_port_handling
- if forwarded = @env["HTTP_X_FORWARDED_HOST"]
- forwarded.split(/,\s?/).last
- elsif http_host = @env['HTTP_HOST']
- http_host
- elsif server_name = @env['SERVER_NAME']
- server_name
- else
- "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
- end
- end
-
- def host
- host_with_port_without_standard_port_handling.sub(/:\d+$/, '')
- end
-
- def port
- if host_with_port_without_standard_port_handling =~ /:(\d+)$/
- $1.to_i
- else
- standard_port
- end
- end
-
- def remote_addr
- @env['REMOTE_ADDR']
- end
-
- def request_method
- @env['REQUEST_METHOD'].downcase.to_sym
- end
-
def server_port
@env['SERVER_PORT'].to_i
end
@@ -189,23 +152,30 @@ end_msg
end
class RackResponse < AbstractResponse #:nodoc:
- attr_accessor :status
-
def initialize(request)
- @request = request
+ @cgi = request.cgi
@writer = lambda { |x| @body << x }
@block = nil
super()
end
+ # Retrieve status from instance variable if has already been delete
+ def status
+ @status || super
+ end
+
def out(output = $stdout, &block)
+ # Nasty hack because CGI sessions are closed after the normal
+ # prepare! statement
+ set_cookies!
+
@block = block
- normalize_headers(@headers)
- if [204, 304].include?(@status.to_i)
- @headers.delete "Content-Type"
- [status, @headers.to_hash, []]
+ @status = headers.delete("Status")
+ if [204, 304].include?(status.to_i)
+ headers.delete("Content-Type")
+ [status, headers.to_hash, []]
else
- [status, @headers.to_hash, self]
+ [status, headers.to_hash, self]
end
end
alias to_a out
@@ -237,43 +207,57 @@ end_msg
@block == nil && @body.empty?
end
- private
- def normalize_headers(options = "text/html")
- if options.is_a?(String)
- headers['Content-Type'] = options unless headers['Content-Type']
- else
- headers['Content-Length'] = options.delete('Content-Length').to_s if options['Content-Length']
+ def prepare!
+ super
- headers['Content-Type'] = options.delete('type') || "text/html"
- headers['Content-Type'] += "; charset=" + options.delete('charset') if options['charset']
+ convert_language!
+ convert_expires!
+ set_status!
+ # set_cookies!
+ end
- headers['Content-Language'] = options.delete('language') if options['language']
- headers['Expires'] = options.delete('expires') if options['expires']
+ private
+ def convert_language!
+ headers["Content-Language"] = headers.delete("language") if headers["language"]
+ end
- @status = options['Status'] || "200 OK"
+ def convert_expires!
+ headers["Expires"] = headers.delete("") if headers["expires"]
+ end
- # Convert 'cookie' header to 'Set-Cookie' headers.
- # Because Set-Cookie header can appear more the once in the response body,
- # we store it in a line break seperated string that will be translated to
- # multiple Set-Cookie header by the handler.
- if cookie = options.delete('cookie')
- cookies = []
+ def convert_content_type!
+ super
+ headers['Content-Type'] = headers.delete('type') || "text/html"
+ headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset']
+ end
- case cookie
- when Array then cookie.each { |c| cookies << c.to_s }
- when Hash then cookie.each { |_, c| cookies << c.to_s }
- else cookies << cookie.to_s
- end
+ def set_content_length!
+ super
+ headers["Content-Length"] = headers["Content-Length"].to_s if headers["Content-Length"]
+ end
- @request.cgi.output_cookies.each { |c| cookies << c.to_s } if @request.cgi.output_cookies
+ def set_status!
+ self.status ||= "200 OK"
+ end
- headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact
+ def set_cookies!
+ # Convert 'cookie' header to 'Set-Cookie' headers.
+ # Because Set-Cookie header can appear more the once in the response body,
+ # we store it in a line break separated string that will be translated to
+ # multiple Set-Cookie header by the handler.
+ if cookie = headers.delete('cookie')
+ cookies = []
+
+ case cookie
+ when Array then cookie.each { |c| cookies << c.to_s }
+ when Hash then cookie.each { |_, c| cookies << c.to_s }
+ else cookies << cookie.to_s
end
- options.each { |k,v| headers[k] = v }
- end
+ @cgi.output_cookies.each { |c| cookies << c.to_s } if @cgi.output_cookies
- ""
+ headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact
+ end
end
end
diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb
index 2d9f6c3e6f..364e6201cc 100755..100644
--- a/actionpack/lib/action_controller/request.rb
+++ b/actionpack/lib/action_controller/request.rb
@@ -2,14 +2,21 @@ require 'tempfile'
require 'stringio'
require 'strscan'
-module ActionController
- # HTTP methods which are accepted by default.
- ACCEPTED_HTTP_METHODS = Set.new(%w( get head put post delete options ))
+require 'active_support/memoizable'
+module ActionController
# CgiRequest and TestRequest provide concrete implementations.
class AbstractRequest
- cattr_accessor :relative_url_root
- remove_method :relative_url_root
+ extend ActiveSupport::Memoizable
+
+ def self.relative_url_root=(*args)
+ ActiveSupport::Deprecation.warn(
+ "ActionController::AbstractRequest.relative_url_root= has been renamed." +
+ "You can now set it with config.action_controller.relative_url_root=", caller)
+ end
+
+ HTTP_METHODS = %w(get head put post delete options)
+ HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h }
# The hash of environment variables for this request,
# such as { 'RAILS_ENV' => 'production' }.
@@ -18,15 +25,12 @@ module ActionController
# The true HTTP request method as a lowercase symbol, such as <tt>:get</tt>.
# UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS.
def request_method
- @request_method ||= begin
- method = ((@env['REQUEST_METHOD'] == 'POST' && !parameters[:_method].blank?) ? parameters[:_method].to_s : @env['REQUEST_METHOD']).downcase
- if ACCEPTED_HTTP_METHODS.include?(method)
- method.to_sym
- else
- raise UnknownHttpMethod, "#{method}, accepted HTTP methods are #{ACCEPTED_HTTP_METHODS.to_a.to_sentence}"
- end
- end
+ method = @env['REQUEST_METHOD']
+ method = parameters[:_method] if method == 'POST' && !parameters[:_method].blank?
+
+ HTTP_METHOD_LOOKUP[method] || raise(UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
end
+ memoize :request_method
# The HTTP request method as a lowercase symbol, such as <tt>:get</tt>.
# Note, HEAD is returned as <tt>:get</tt> since the two are functionally
@@ -61,36 +65,62 @@ module ActionController
request_method == :head
end
- # Provides acccess to the request's HTTP headers, for example:
+ # Provides access to the request's HTTP headers, for example:
# request.headers["Content-Type"] # => "text/plain"
def headers
- @headers ||= ActionController::Http::Headers.new(@env)
+ ActionController::Http::Headers.new(@env)
end
+ memoize :headers
def content_length
- @content_length ||= env['CONTENT_LENGTH'].to_i
+ @env['CONTENT_LENGTH'].to_i
end
+ memoize :content_length
# The MIME type of the HTTP request, such as Mime::XML.
#
# For backward compatibility, the post format is extracted from the
# X-Post-Data-Format HTTP header if present.
def content_type
- @content_type ||= Mime::Type.lookup(content_type_without_parameters)
+ Mime::Type.lookup(content_type_without_parameters)
end
+ memoize :content_type
# Returns the accepted MIME type for the request
def accepts
- @accepts ||=
- begin
- header = @env['HTTP_ACCEPT'].to_s.strip
+ header = @env['HTTP_ACCEPT'].to_s.strip
- if header.empty?
- [content_type, Mime::ALL].compact
- else
- Mime::Type.parse(header)
- end
- end
+ if header.empty?
+ [content_type, Mime::ALL].compact
+ else
+ Mime::Type.parse(header)
+ end
+ end
+ memoize :accepts
+
+ def if_modified_since
+ if since = env['HTTP_IF_MODIFIED_SINCE']
+ Time.rfc2822(since) rescue nil
+ end
+ end
+ memoize :if_modified_since
+
+ def if_none_match
+ env['HTTP_IF_NONE_MATCH']
+ end
+
+ def not_modified?(modified_at)
+ if_modified_since && modified_at && if_modified_since >= modified_at
+ end
+
+ def etag_matches?(etag)
+ if_none_match && if_none_match == etag
+ end
+
+ # Check response freshness (Last-Modified and ETag) against request
+ # If-Modified-Since and If-None-Match conditions.
+ def fresh?(response)
+ not_modified?(response.last_modified) || etag_matches?(response.etag)
end
# Returns the Mime type for the format used in the request.
@@ -99,7 +129,7 @@ module ActionController
# GET /posts/5.xhtml | request.format => Mime::HTML
# GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt>
def format
- @format ||= begin
+ @format ||=
if parameters[:format]
Mime::Type.lookup_by_extension(parameters[:format])
elsif ActionController::Base.use_accept_header
@@ -109,16 +139,15 @@ module ActionController
else
Mime::Type.lookup_by_extension("html")
end
- end
end
-
-
+
+
# Sets the format by string extension, which can be used to force custom formats that are not controlled by the extension.
# Example:
#
# class ApplicationController < ActionController::Base
# before_filter :adjust_format_for_iphone
- #
+ #
# private
# def adjust_format_for_iphone
# request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
@@ -197,42 +226,62 @@ EOM
@env['REMOTE_ADDR']
end
+ memoize :remote_ip
# Returns the lowercase name of the HTTP server software.
def server_software
(@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
end
+ memoize :server_software
# Returns the complete URL used for this request
def url
protocol + host_with_port + request_uri
end
+ memoize :url
# Return 'https://' if this is an SSL request and 'http://' otherwise.
def protocol
ssl? ? 'https://' : 'http://'
end
+ memoize :protocol
# Is this an SSL request?
def ssl?
@env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
end
+ def raw_host_with_port
+ if forwarded = env["HTTP_X_FORWARDED_HOST"]
+ forwarded.split(/,\s?/).last
+ else
+ env['HTTP_HOST'] || env['SERVER_NAME'] || "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
+ end
+ end
+
# Returns the host for this request, such as example.com.
def host
+ raw_host_with_port.sub(/:\d+$/, '')
end
+ memoize :host
# Returns a host:port string for this request, such as example.com or
# example.com:8080.
def host_with_port
- @host_with_port ||= host + port_string
+ "#{host}#{port_string}"
end
+ memoize :host_with_port
# Returns the port number of this request as an integer.
def port
- @port_as_int ||= @env['SERVER_PORT'].to_i
+ if raw_host_with_port =~ /:(\d+)$/
+ $1.to_i
+ else
+ standard_port
+ end
end
+ memoize :port
# Returns the standard port number for this request's protocol
def standard_port
@@ -245,7 +294,7 @@ EOM
# Returns a port suffix like ":8080" if the port number of this request
# is not the default HTTP port 80 or HTTPS port 443.
def port_string
- (port == standard_port) ? '' : ":#{port}"
+ port == standard_port ? '' : ":#{port}"
end
# Returns the domain part of a host, such as rubyonrails.org in "www.rubyonrails.org". You can specify
@@ -265,7 +314,7 @@ EOM
parts[0..-(tld_length+2)]
end
- # Return the query string, accounting for server idiosyncracies.
+ # Return the query string, accounting for server idiosyncrasies.
def query_string
if uri = @env['REQUEST_URI']
uri.split('?', 2)[1] || ''
@@ -273,8 +322,9 @@ EOM
@env['QUERY_STRING'] || ''
end
end
+ memoize :query_string
- # Return the request URI, accounting for server idiosyncracies.
+ # Return the request URI, accounting for server idiosyncrasies.
# WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
def request_uri
if uri = @env['REQUEST_URI']
@@ -282,46 +332,33 @@ EOM
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
else
# Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
- script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
- uri = @env['PATH_INFO']
- uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil?
- unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty?
- uri << '?' << env_qs
+ uri = @env['PATH_INFO'].to_s
+
+ if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
+ uri = uri.sub(/#{script_filename}\//, '')
end
- if uri.nil?
+ env_qs = @env['QUERY_STRING'].to_s
+ uri += "?#{env_qs}" unless env_qs.empty?
+
+ if uri.blank?
@env.delete('REQUEST_URI')
- uri
else
@env['REQUEST_URI'] = uri
end
end
end
+ memoize :request_uri
# Returns the interpreted path to requested resource after all the installation directory of this application was taken into account
def path
path = (uri = request_uri) ? uri.split('?').first.to_s : ''
# Cut off the path to the installation directory if given
- path.sub!(%r/^#{relative_url_root}/, '')
- path || ''
- end
-
- # Returns the path minus the web server relative installation directory.
- # This can be set with the environment variable RAILS_RELATIVE_URL_ROOT.
- # It can be automatically extracted for Apache setups. If the server is not
- # Apache, this method returns an empty string.
- def relative_url_root
- @@relative_url_root ||= case
- when @env["RAILS_RELATIVE_URL_ROOT"]
- @env["RAILS_RELATIVE_URL_ROOT"]
- when server_software == 'apache'
- @env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '')
- else
- ''
- end
+ path.sub!(%r/^#{ActionController::Base.relative_url_root}/, '')
+ path || ''
end
-
+ memoize :path
# Read the request body. This is useful for web services that need to
# work with raw requests directly.
@@ -343,34 +380,56 @@ EOM
@symbolized_path_parameters = @parameters = nil
end
- # The same as <tt>path_parameters</tt> with explicitly symbolized keys
- def symbolized_path_parameters
+ # The same as <tt>path_parameters</tt> with explicitly symbolized keys
+ def symbolized_path_parameters
@symbolized_path_parameters ||= path_parameters.symbolize_keys
end
# Returns a hash with the parameters used to form the path of the request.
# Returned hash keys are strings. See <tt>symbolized_path_parameters</tt> for symbolized keys.
#
- # Example:
+ # Example:
#
# {'action' => 'my_action', 'controller' => 'my_controller'}
def path_parameters
@path_parameters ||= {}
end
+ # The request body is an IO input stream. If the RAW_POST_DATA environment
+ # variable is already set, wrap it in a StringIO.
+ def body
+ if raw_post = env['RAW_POST_DATA']
+ raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
+ StringIO.new(raw_post)
+ else
+ body_stream
+ end
+ end
- #--
- # Must be implemented in the concrete request
- #++
+ def remote_addr
+ @env['REMOTE_ADDR']
+ end
- # The request body is an IO input stream.
- def body
+ def referrer
+ @env['HTTP_REFERER']
+ end
+ alias referer referrer
+
+
+ def query_parameters
+ @query_parameters ||= self.class.parse_query_parameters(query_string)
end
- def query_parameters #:nodoc:
+ def request_parameters
+ @request_parameters ||= parse_formatted_request_parameters
end
- def request_parameters #:nodoc:
+
+ #--
+ # Must be implemented in the concrete request
+ #++
+
+ def body_stream #:nodoc:
end
def cookies #:nodoc:
@@ -397,8 +456,9 @@ EOM
# The raw content type string with its parameters stripped off.
def content_type_without_parameters
- @content_type_without_parameters ||= self.class.extract_content_type_without_parameters(content_type_with_parameters)
+ self.class.extract_content_type_without_parameters(content_type_with_parameters)
end
+ memoize :content_type_without_parameters
private
def content_type_from_legacy_post_data_format_header
diff --git a/actionpack/lib/action_controller/request_forgery_protection.rb b/actionpack/lib/action_controller/request_forgery_protection.rb
index 02c9d59d07..05a6d8bb79 100644
--- a/actionpack/lib/action_controller/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/request_forgery_protection.rb
@@ -17,7 +17,7 @@ module ActionController #:nodoc:
# forged link from another site, is done by embedding a token based on the session (which an attacker wouldn't know) in all
# forms and Ajax requests generated by Rails and then verifying the authenticity of that token in the controller. Only
# HTML/JavaScript requests are checked, so this will not protect your XML API (presumably you'll have a different authentication
- # scheme there anyway). Also, GET requests are not protected as these should be indempotent anyway.
+ # scheme there anyway). Also, GET requests are not protected as these should be idempotent anyway.
#
# This is turned on with the <tt>protect_from_forgery</tt> method, which will check the token and raise an
# ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the error message in
diff --git a/actionpack/lib/action_controller/request_profiler.rb b/actionpack/lib/action_controller/request_profiler.rb
index a6471d0c08..a6471d0c08 100755..100644
--- a/actionpack/lib/action_controller/request_profiler.rb
+++ b/actionpack/lib/action_controller/request_profiler.rb
diff --git a/actionpack/lib/action_controller/rescue.rb b/actionpack/lib/action_controller/rescue.rb
index 163ed87fbb..a1a9d68a35 100644
--- a/actionpack/lib/action_controller/rescue.rb
+++ b/actionpack/lib/action_controller/rescue.rb
@@ -112,19 +112,23 @@ module ActionController #:nodoc:
protected
# Exception handler called when the performance of an action raises an exception.
def rescue_action(exception)
- log_error(exception) if logger
- erase_results if performed?
+ if handler_for_rescue(exception)
+ rescue_action_with_handler(exception)
+ else
+ log_error(exception) if logger
+ erase_results if performed?
- # Let the exception alter the response if it wants.
- # For example, MethodNotAllowed sets the Allow header.
- if exception.respond_to?(:handle_response!)
- exception.handle_response!(response)
- end
+ # Let the exception alter the response if it wants.
+ # For example, MethodNotAllowed sets the Allow header.
+ if exception.respond_to?(:handle_response!)
+ exception.handle_response!(response)
+ end
- if consider_all_requests_local || local_request?
- rescue_action_locally(exception)
- else
- rescue_action_in_public(exception)
+ if consider_all_requests_local || local_request?
+ rescue_action_locally(exception)
+ else
+ rescue_action_in_public(exception)
+ end
end
end
@@ -178,7 +182,7 @@ module ActionController #:nodoc:
@template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub")))
@template.send!(:assign_variables_from_controller)
- @template.instance_variable_set("@contents", @template.render(:file => template_path_for_local_rescue(exception), :use_full_path => false))
+ @template.instance_variable_set("@contents", @template.render(:file => template_path_for_local_rescue(exception)))
response.content_type = Mime::HTML
render_for_file(rescues_path("layout"), response_code_for_rescue(exception))
@@ -200,7 +204,7 @@ module ActionController #:nodoc:
def perform_action_with_rescue #:nodoc:
perform_action_without_rescue
rescue Exception => exception
- rescue_action_with_handler(exception) || rescue_action(exception)
+ rescue_action(exception)
end
def rescues_path(template_name)
diff --git a/actionpack/lib/action_controller/resources.rb b/actionpack/lib/action_controller/resources.rb
index af2fcaf3ad..5f579cdb11 100644
--- a/actionpack/lib/action_controller/resources.rb
+++ b/actionpack/lib/action_controller/resources.rb
@@ -14,10 +14,10 @@ module ActionController
#
# === The Different Methods and their Usage
#
- # +GET+ Requests for a resource, no saving or editing of a resource should occur in a GET request
- # +POST+ Creation of resources
- # +PUT+ Editing of attributes on a resource
- # +DELETE+ Deletion of a resource
+ # [+GET+] Requests for a resource, no saving or editing of a resource should occur in a GET request
+ # [+POST+] Creation of resources
+ # [+PUT+] Editing of attributes on a resource
+ # [+DELETE+] Deletion of a resource
#
# === Examples
#
@@ -296,6 +296,10 @@ module ActionController
# article_comments_url(:article_id => @article)
# article_comment_url(:article_id => @article, :id => @comment)
#
+ # If you don't want to load all objects from the database you might want to use the <tt>article_id</tt> directly:
+ #
+ # articles_comments_url(@comment.article_id, @comment)
+ #
# * <tt>:name_prefix</tt> - Define a prefix for all generated routes, usually ending in an underscore.
# Use this if you have named routes that may clash.
#
@@ -303,13 +307,13 @@ module ActionController
# map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_'
#
# You may also use <tt>:name_prefix</tt> to override the generic named routes in a nested resource:
- #
+ #
# map.resources :articles do |article|
# article.resources :comments, :name_prefix => nil
- # end
- #
+ # end
+ #
# This will yield named resources like so:
- #
+ #
# comments_url(@article)
# comment_url(@article, @comment)
#
@@ -477,8 +481,7 @@ module ActionController
resource.collection_methods.each do |method, actions|
actions.each do |action|
action_options = action_options_for(action, resource, method)
- map.named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options)
- map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}.:format", action_options)
+ map_named_routes(map, "#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options)
end
end
end
@@ -491,18 +494,15 @@ module ActionController
index_route_name << "_index"
end
- map.named_route(index_route_name, resource.path, index_action_options)
- map.named_route("formatted_#{index_route_name}", "#{resource.path}.:format", index_action_options)
+ map_named_routes(map, index_route_name, resource.path, index_action_options)
create_action_options = action_options_for("create", resource)
- map.connect(resource.path, create_action_options)
- map.connect("#{resource.path}.:format", create_action_options)
+ map_unnamed_routes(map, resource.path, create_action_options)
end
def map_default_singleton_actions(map, resource)
create_action_options = action_options_for("create", resource)
- map.connect(resource.path, create_action_options)
- map.connect("#{resource.path}.:format", create_action_options)
+ map_unnamed_routes(map, resource.path, create_action_options)
end
def map_new_actions(map, resource)
@@ -510,11 +510,9 @@ module ActionController
actions.each do |action|
action_options = action_options_for(action, resource, method)
if action == :new
- map.named_route("new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options)
- map.named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}.:format", action_options)
+ map_named_routes(map, "new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options)
else
- map.named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options)
- map.named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}.:format", action_options)
+ map_named_routes(map, "#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options)
end
end
end
@@ -528,22 +526,28 @@ module ActionController
action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
action_path ||= Base.resources_path_names[action] || action
- map.named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}", action_options)
- map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}.:format",action_options)
+ map_named_routes(map, "#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}", action_options)
end
end
show_action_options = action_options_for("show", resource)
- map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options)
- map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options)
+ map_named_routes(map, "#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options)
update_action_options = action_options_for("update", resource)
- map.connect(resource.member_path, update_action_options)
- map.connect("#{resource.member_path}.:format", update_action_options)
+ map_unnamed_routes(map, resource.member_path, update_action_options)
destroy_action_options = action_options_for("destroy", resource)
- map.connect(resource.member_path, destroy_action_options)
- map.connect("#{resource.member_path}.:format", destroy_action_options)
+ map_unnamed_routes(map, resource.member_path, destroy_action_options)
+ end
+
+ def map_unnamed_routes(map, path_without_format, options)
+ map.connect(path_without_format, options)
+ map.connect("#{path_without_format}.:format", options)
+ end
+
+ def map_named_routes(map, name, path_without_format, options)
+ map.named_route(name, path_without_format, options)
+ map.named_route("formatted_#{name}", "#{path_without_format}.:format", options)
end
def add_conditions_for(conditions, method)
@@ -555,6 +559,7 @@ module ActionController
def action_options_for(action, resource, method = nil)
default_options = { :action => action.to_s }
require_id = !resource.kind_of?(SingletonResource)
+
case default_options[:action]
when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements)
when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements)
@@ -569,4 +574,4 @@ end
class ActionController::Routing::RouteSet::Mapper
include ActionController::Resources
-end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb
index 1d9f6676ba..5dac4128bb 100755..100644
--- a/actionpack/lib/action_controller/response.rb
+++ b/actionpack/lib/action_controller/response.rb
@@ -1,58 +1,161 @@
require 'digest/md5'
-module ActionController
- class AbstractResponse #:nodoc:
+module ActionController # :nodoc:
+ # Represents an HTTP response generated by a controller action. One can use an
+ # ActionController::AbstractResponse object to retrieve the current state of the
+ # response, or customize the response. An AbstractResponse object can either
+ # represent a "real" HTTP response (i.e. one that is meant to be sent back to the
+ # web browser) or a test response (i.e. one that is generated from integration
+ # tests). See CgiResponse and TestResponse, respectively.
+ #
+ # AbstractResponse is mostly a Ruby on Rails framework implement detail, and should
+ # never be used directly in controllers. Controllers should use the methods defined
+ # in ActionController::Base instead. For example, if you want to set the HTTP
+ # response's content MIME type, then use ActionControllerBase#headers instead of
+ # AbstractResponse#headers.
+ #
+ # Nevertheless, integration tests may want to inspect controller responses in more
+ # detail, and that's when AbstractResponse can be useful for application developers.
+ # Integration test methods such as ActionController::Integration::Session#get and
+ # ActionController::Integration::Session#post return objects of type TestResponse
+ # (which are of course also of type AbstractResponse).
+ #
+ # For example, the following demo integration "test" prints the body of the
+ # controller response to the console:
+ #
+ # class DemoControllerTest < ActionController::IntegrationTest
+ # def test_print_root_path_to_console
+ # get('/')
+ # puts @response.body
+ # end
+ # end
+ class AbstractResponse
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
attr_accessor :request
- attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout
+
+ # The body content (e.g. HTML) of the response, as a String.
+ attr_accessor :body
+ # The headers of the response, as a Hash. It maps header names to header values.
+ attr_accessor :headers
+ attr_accessor :session, :cookies, :assigns, :template, :layout
+ attr_accessor :redirected_to, :redirected_to_method_params
+
+ delegate :default_charset, :to => 'ActionController::Base'
def initialize
@body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
end
+ def status; headers['Status'] end
+ def status=(status) headers['Status'] = status end
+
+ def location; headers['Location'] end
+ def location=(url) headers['Location'] = url end
+
+
+ # Sets the HTTP response's content MIME type. For example, in the controller
+ # you could write this:
+ #
+ # response.content_type = "text/plain"
+ #
+ # If a character set has been defined for this response (see charset=) then
+ # the character set information will also be included in the content type
+ # information.
def content_type=(mime_type)
- self.headers["Content-Type"] = charset ? "#{mime_type}; charset=#{charset}" : mime_type
+ self.headers["Content-Type"] =
+ if mime_type =~ /charset/ || (c = charset).nil?
+ mime_type.to_s
+ else
+ "#{mime_type}; charset=#{c}"
+ end
end
-
+
+ # Returns the response's content MIME type, or nil if content type has been set.
def content_type
content_type = String(headers["Content-Type"] || headers["type"]).split(";")[0]
content_type.blank? ? nil : content_type
end
-
- def charset=(encoding)
- self.headers["Content-Type"] = "#{content_type || Mime::HTML}; charset=#{encoding}"
+
+ # Set the charset of the Content-Type header. Set to nil to remove it.
+ # If no content type is set, it defaults to HTML.
+ def charset=(charset)
+ headers["Content-Type"] =
+ if charset
+ "#{content_type || Mime::HTML}; charset=#{charset}"
+ else
+ content_type || Mime::HTML.to_s
+ end
end
-
+
def charset
charset = String(headers["Content-Type"] || headers["type"]).split(";")[1]
charset.blank? ? nil : charset.strip.split("=")[1]
end
- def redirect(to_url, response_status)
- self.headers["Status"] = response_status
- self.headers["Location"] = to_url
+ def last_modified
+ if last = headers['Last-Modified']
+ Time.httpdate(last)
+ end
+ end
+
+ def last_modified?
+ headers.include?('Last-Modified')
+ end
+
+ def last_modified=(utc_time)
+ headers['Last-Modified'] = utc_time.httpdate
+ end
+
+ def etag; headers['ETag'] end
+ def etag?; headers.include?('ETag') end
+ def etag=(etag)
+ headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
+ end
+
+ def redirect(url, status)
+ self.status = status
+ self.location = url
+ self.body = "<html><body>You are being <a href=\"#{url}\">redirected</a>.</body></html>"
+ end
- self.body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>"
+ def sending_file?
+ headers["Content-Transfer-Encoding"] == "binary"
+ end
+
+ def assign_default_content_type_and_charset!
+ self.content_type ||= Mime::HTML
+ self.charset ||= default_charset unless sending_file?
end
def prepare!
+ assign_default_content_type_and_charset!
+ set_content_length!
handle_conditional_get!
convert_content_type!
- set_content_length!
end
-
private
def handle_conditional_get!
- if body.is_a?(String) && (headers['Status'] ? headers['Status'][0..2] == '200' : true) && !body.empty?
- self.headers['ETag'] ||= %("#{Digest::MD5.hexdigest(body)}")
- self.headers['Cache-Control'] = 'private, max-age=0, must-revalidate' if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
-
- if request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag']
- self.headers['Status'] = '304 Not Modified'
+ if nonempty_ok_response?
+ self.etag ||= body
+ if request && request.etag_matches?(etag)
+ self.status = '304 Not Modified'
self.body = ''
end
end
+
+ set_conditional_cache_control! if etag? || last_modified?
+ end
+
+ def nonempty_ok_response?
+ ok = !status || status[0..2] == '200'
+ ok && body.is_a?(String) && !body.empty?
+ end
+
+ def set_conditional_cache_control!
+ if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
+ headers['Cache-Control'] = 'private, max-age=0, must-revalidate'
+ end
end
def convert_content_type!
@@ -70,7 +173,9 @@ module ActionController
# Don't set the Content-Length for block-based bodies as that would mean reading it all into memory. Not nice
# for, say, a 2GB streaming file.
def set_content_length!
- self.headers["Content-Length"] = body.size unless body.respond_to?(:call)
+ unless body.respond_to?(:call) || (status && status[0..2] == '304')
+ self.headers["Content-Length"] ||= body.size
+ end
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb
index dfbaa53b7c..8d51e823a6 100644
--- a/actionpack/lib/action_controller/routing.rb
+++ b/actionpack/lib/action_controller/routing.rb
@@ -201,7 +201,7 @@ module ActionController
# With conditions you can define restrictions on routes. Currently the only valid condition is <tt>:method</tt>.
#
# * <tt>:method</tt> - Allows you to specify which method can access the route. Possible values are <tt>:post</tt>,
- # <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. The default value is <tt>:any</tt>,
+ # <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. The default value is <tt>:any</tt>,
# <tt>:any</tt> means that any method can access the route.
#
# Example:
@@ -213,7 +213,7 @@ module ActionController
#
# Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
# URL will route to the <tt>show</tt> action.
- #
+ #
# == Reloading routes
#
# You can reload routes if you feel you must:
@@ -281,9 +281,9 @@ module ActionController
end
class << self
- # Expects an array of controller names as the first argument.
- # Executes the passed block with only the named controllers named available.
- # This method is used in internal Rails testing.
+ # Expects an array of controller names as the first argument.
+ # Executes the passed block with only the named controllers named available.
+ # This method is used in internal Rails testing.
def with_controllers(names)
prior_controllers = @possible_controllers
use_controllers! names
@@ -292,10 +292,10 @@ module ActionController
use_controllers! prior_controllers
end
- # Returns an array of paths, cleaned of double-slashes and relative path references.
- # * "\\\" and "//" become "\\" or "/".
- # * "/foo/bar/../config" becomes "/foo/config".
- # The returned array is sorted by length, descending.
+ # Returns an array of paths, cleaned of double-slashes and relative path references.
+ # * "\\\" and "//" become "\\" or "/".
+ # * "/foo/bar/../config" becomes "/foo/config".
+ # The returned array is sorted by length, descending.
def normalize_paths(paths)
# do the hokey-pokey of path normalization...
paths = paths.collect do |path|
@@ -314,7 +314,7 @@ module ActionController
paths = paths.uniq.sort_by { |path| - path.length }
end
- # Returns the array of controller names currently available to ActionController::Routing.
+ # Returns the array of controller names currently available to ActionController::Routing.
def possible_controllers
unless @possible_controllers
@possible_controllers = []
@@ -339,28 +339,27 @@ module ActionController
@possible_controllers
end
- # Replaces the internal list of controllers available to ActionController::Routing with the passed argument.
- # ActionController::Routing.use_controllers!([ "posts", "comments", "admin/comments" ])
+ # Replaces the internal list of controllers available to ActionController::Routing with the passed argument.
+ # ActionController::Routing.use_controllers!([ "posts", "comments", "admin/comments" ])
def use_controllers!(controller_names)
@possible_controllers = controller_names
end
- # Returns a controller path for a new +controller+ based on a +previous+ controller path.
- # Handles 4 scenarios:
- #
- # * stay in the previous controller:
- # controller_relative_to( nil, "groups/discussion" ) # => "groups/discussion"
- #
- # * stay in the previous namespace:
- # controller_relative_to( "posts", "groups/discussion" ) # => "groups/posts"
- #
- # * forced move to the root namespace:
- # controller_relative_to( "/posts", "groups/discussion" ) # => "posts"
- #
- # * previous namespace is root:
- # controller_relative_to( "posts", "anything_with_no_slashes" ) # =>"posts"
- #
-
+ # Returns a controller path for a new +controller+ based on a +previous+ controller path.
+ # Handles 4 scenarios:
+ #
+ # * stay in the previous controller:
+ # controller_relative_to( nil, "groups/discussion" ) # => "groups/discussion"
+ #
+ # * stay in the previous namespace:
+ # controller_relative_to( "posts", "groups/discussion" ) # => "groups/posts"
+ #
+ # * forced move to the root namespace:
+ # controller_relative_to( "/posts", "groups/discussion" ) # => "posts"
+ #
+ # * previous namespace is root:
+ # controller_relative_to( "posts", "anything_with_no_slashes" ) # =>"posts"
+ #
def controller_relative_to(controller, previous)
if controller.nil? then previous
elsif controller[0] == ?/ then controller[1..-1]
@@ -369,12 +368,11 @@ module ActionController
end
end
end
-
Routes = RouteSet.new
ActiveSupport::Inflector.module_eval do
- # Ensures that routes are reloaded when Rails inflections are updated.
+ # Ensures that routes are reloaded when Rails inflections are updated.
def inflections_with_route_reloading(&block)
returning(inflections_without_route_reloading(&block)) {
ActionController::Routing::Routes.reload! if block_given?
diff --git a/actionpack/lib/action_controller/routing/builder.rb b/actionpack/lib/action_controller/routing/builder.rb
index b8323847fd..03427e41de 100644
--- a/actionpack/lib/action_controller/routing/builder.rb
+++ b/actionpack/lib/action_controller/routing/builder.rb
@@ -48,14 +48,10 @@ module ActionController
end
when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true)
when /\A\?(.*?)\?/
- returning segment = StaticSegment.new($1) do
- segment.is_optional = true
- end
+ StaticSegment.new($1, :optional => true)
when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1)
when Regexp.new(separator_pattern) then
- returning segment = DividerSegment.new($&) do
- segment.is_optional = (optional_separators.include? $&)
- end
+ DividerSegment.new($&, :optional => (optional_separators.include? $&))
end
[segment, $~.post_match]
end
@@ -76,6 +72,8 @@ module ActionController
defaults = (options.delete(:defaults) || {}).dup
conditions = (options.delete(:conditions) || {}).dup
+ validate_route_conditions(conditions)
+
path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact
options.each do |key, value|
hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements
@@ -174,30 +172,30 @@ module ActionController
defaults, requirements, conditions = divide_route_options(segments, options)
requirements = assign_route_options(segments, defaults, requirements)
- route = Route.new
-
- route.segments = segments
- route.requirements = requirements
- route.conditions = conditions
+ # TODO: Segments should be frozen on initialize
+ segments.each { |segment| segment.freeze }
- if !route.significant_keys.include?(:action) && !route.requirements[:action]
- route.requirements[:action] = "index"
- route.significant_keys << :action
- end
-
- # Routes cannot use the current string interpolation method
- # if there are user-supplied <tt>:requirements</tt> as the interpolation
- # code won't raise RoutingErrors when generating
- if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
- route.optimise = false
- end
+ route = Route.new(segments, requirements, conditions)
if !route.significant_keys.include?(:controller)
raise ArgumentError, "Illegal route: the :controller must be specified!"
end
- route
+ route.freeze
end
+
+ private
+ def validate_route_conditions(conditions)
+ if method = conditions[:method]
+ if method == :head
+ raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers"
+ end
+
+ unless HTTP_METHODS.include?(method.to_sym)
+ raise ArgumentError, "Invalid HTTP method specified in route conditions: #{conditions.inspect}"
+ end
+ end
+ end
end
end
end
diff --git a/actionpack/lib/action_controller/routing/optimisations.rb b/actionpack/lib/action_controller/routing/optimisations.rb
index cd4a423e6b..0fe836606c 100644
--- a/actionpack/lib/action_controller/routing/optimisations.rb
+++ b/actionpack/lib/action_controller/routing/optimisations.rb
@@ -1,14 +1,14 @@
module ActionController
module Routing
- # Much of the slow performance from routes comes from the
+ # Much of the slow performance from routes comes from the
# complexity of expiry, <tt>:requirements</tt> matching, defaults providing
- # and figuring out which url pattern to use. With named routes
- # we can avoid the expense of finding the right route. So if
+ # and figuring out which url pattern to use. With named routes
+ # we can avoid the expense of finding the right route. So if
# they've provided the right number of arguments, and have no
# <tt>:requirements</tt>, we can just build up a string and return it.
- #
- # To support building optimisations for other common cases, the
- # generation code is separated into several classes
+ #
+ # To support building optimisations for other common cases, the
+ # generation code is separated into several classes
module Optimisation
def generate_optimisation_block(route, kind)
return "" unless route.optimise?
@@ -20,6 +20,7 @@ module ActionController
class Optimiser
attr_reader :route, :kind
+
def initialize(route, kind)
@route = route
@kind = kind
@@ -53,12 +54,12 @@ module ActionController
# map.person '/people/:id'
#
# If the user calls <tt>person_url(@person)</tt>, we can simply
- # return a string like "/people/#{@person.to_param}"
+ # return a string like "/people/#{@person.to_param}"
# rather than triggering the expensive logic in +url_for+.
class PositionalArguments < Optimiser
def guard_condition
number_of_arguments = route.segment_keys.size
- # if they're using foo_url(:id=>2) it's one
+ # if they're using foo_url(:id=>2) it's one
# argument, but we don't want to generate /foos/id2
if number_of_arguments == 1
"(!defined?(default_url_options) || default_url_options.blank?) && defined?(request) && request && args.size == 1 && !args.first.is_a?(Hash)"
@@ -76,7 +77,7 @@ module ActionController
elements << '#{request.host_with_port}'
end
- elements << '#{request.relative_url_root if request.relative_url_root}'
+ elements << '#{ActionController::Base.relative_url_root if ActionController::Base.relative_url_root}'
# The last entry in <tt>route.segments</tt> appears to *always* be a
# 'divider segment' for '/' but we have assertions to ensure that
@@ -94,14 +95,14 @@ module ActionController
end
# This case is mostly the same as the positional arguments case
- # above, but it supports additional query parameters as the last
+ # above, but it supports additional query parameters as the last
# argument
class PositionalArgumentsWithAdditionalParams < PositionalArguments
def guard_condition
"(!defined?(default_url_options) || default_url_options.blank?) && defined?(request) && request && args.size == #{route.segment_keys.size + 1} && !args.last.has_key?(:anchor) && !args.last.has_key?(:port) && !args.last.has_key?(:host)"
end
- # This case uses almost the same code as positional arguments,
+ # This case uses almost the same code as positional arguments,
# but add an args.last.to_query on the end
def generation_code
super.insert(-2, '?#{args.last.to_query}')
@@ -110,7 +111,7 @@ module ActionController
# To avoid generating "http://localhost/?host=foo.example.com" we
# can't use this optimisation on routes without any segments
def applicable?
- super && route.segment_keys.size > 0
+ super && route.segment_keys.size > 0
end
end
diff --git a/actionpack/lib/action_controller/routing/recognition_optimisation.rb b/actionpack/lib/action_controller/routing/recognition_optimisation.rb
index cf8f5232c1..6d54d0334c 100644
--- a/actionpack/lib/action_controller/routing/recognition_optimisation.rb
+++ b/actionpack/lib/action_controller/routing/recognition_optimisation.rb
@@ -51,7 +51,6 @@ module ActionController
# 3) segm test for /users/:id
# (jump to list index = 5)
# 4) full test for /users/:id => here we are!
-
class RouteSet
def recognize_path(path, environment={})
result = recognize_optimized(path, environment) and return result
@@ -68,28 +67,6 @@ module ActionController
end
end
- def recognize_optimized(path, env)
- write_recognize_optimized
- recognize_optimized(path, env)
- end
-
- def write_recognize_optimized
- tree = segment_tree(routes)
- body = generate_code(tree)
- instance_eval %{
- def recognize_optimized(path, env)
- segments = to_plain_segments(path)
- index = #{body}
- return nil unless index
- while index < routes.size
- result = routes[index].recognize(path, env) and return result
- index += 1
- end
- nil
- end
- }, __FILE__, __LINE__
- end
-
def segment_tree(routes)
tree = [0]
@@ -153,6 +130,23 @@ module ActionController
segments
end
+ private
+ def write_recognize_optimized!
+ tree = segment_tree(routes)
+ body = generate_code(tree)
+ instance_eval %{
+ def recognize_optimized(path, env)
+ segments = to_plain_segments(path)
+ index = #{body}
+ return nil unless index
+ while index < routes.size
+ result = routes[index].recognize(path, env) and return result
+ index += 1
+ end
+ nil
+ end
+ }, __FILE__, __LINE__
+ end
end
end
end
diff --git a/actionpack/lib/action_controller/routing/route.rb b/actionpack/lib/action_controller/routing/route.rb
index a0d108ba03..2106ac09e0 100644
--- a/actionpack/lib/action_controller/routing/route.rb
+++ b/actionpack/lib/action_controller/routing/route.rb
@@ -3,11 +3,25 @@ module ActionController
class Route #:nodoc:
attr_accessor :segments, :requirements, :conditions, :optimise
- def initialize
- @segments = []
- @requirements = {}
- @conditions = {}
- @optimise = true
+ def initialize(segments = [], requirements = {}, conditions = {})
+ @segments = segments
+ @requirements = requirements
+ @conditions = conditions
+
+ if !significant_keys.include?(:action) && !requirements[:action]
+ @requirements[:action] = "index"
+ @significant_keys << :action
+ end
+
+ # Routes cannot use the current string interpolation method
+ # if there are user-supplied <tt>:requirements</tt> as the interpolation
+ # code won't raise RoutingErrors when generating
+ has_requirements = @segments.detect { |segment| segment.respond_to?(:regexp) && segment.regexp }
+ if has_requirements || @requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
+ @optimise = false
+ else
+ @optimise = true
+ end
end
# Indicates whether the routes should be optimised with the string interpolation
@@ -22,129 +36,6 @@ module ActionController
end.compact
end
- # Write and compile a +generate+ method for this Route.
- def write_generation
- # Build the main body of the generation
- body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
-
- # If we have conditions that must be tested first, nest the body inside an if
- body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
- args = "options, hash, expire_on = {}"
-
- # Nest the body inside of a def block, and then compile it.
- raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
-
- # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
- # are the same as the keys that were recalled from the previous request. Thus,
- # we can use the expire_on.keys to determine which keys ought to be used to build
- # the query string. (Never use keys from the recalled request when building the
- # query string.)
-
- method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
-
- method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
- raw_method
- end
-
- # Build several lines of code that extract values from the options hash. If any
- # of the values are missing or rejected then a return will be executed.
- def generation_extraction
- segments.collect do |segment|
- segment.extraction_code
- end.compact * "\n"
- end
-
- # Produce a condition expression that will check the requirements of this route
- # upon generation.
- def generation_requirements
- requirement_conditions = requirements.collect do |key, req|
- if req.is_a? Regexp
- value_regexp = Regexp.new "\\A#{req.to_s}\\Z"
- "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
- else
- "hash[:#{key}] == #{req.inspect}"
- end
- end
- requirement_conditions * ' && ' unless requirement_conditions.empty?
- end
-
- def generation_structure
- segments.last.string_structure segments[0..-2]
- end
-
- # Write and compile a +recognize+ method for this Route.
- def write_recognition
- # Create an if structure to extract the params from a match if it occurs.
- body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
- body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
-
- # Build the method declaration and compile it
- method_decl = "def recognize(path, env={})\n#{body}\nend"
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
- method_decl
- end
-
- # Plugins may override this method to add other conditions, like checks on
- # host, subdomain, and so forth. Note that changes here only affect route
- # recognition, not generation.
- def recognition_conditions
- result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
- result << "conditions[:method] === env[:method]" if conditions[:method]
- result
- end
-
- # Build the regular expression pattern that will match this route.
- def recognition_pattern(wrap = true)
- pattern = ''
- segments.reverse_each do |segment|
- pattern = segment.build_pattern pattern
- end
- wrap ? ("\\A" + pattern + "\\Z") : pattern
- end
-
- # Write the code to extract the parameters from a matched route.
- def recognition_extraction
- next_capture = 1
- extraction = segments.collect do |segment|
- x = segment.match_extraction(next_capture)
- next_capture += Regexp.new(segment.regexp_chunk).number_of_captures
- x
- end
- extraction.compact
- end
-
- # Write the real generation implementation and then resend the message.
- def generate(options, hash, expire_on = {})
- write_generation
- generate options, hash, expire_on
- end
-
- def generate_extras(options, hash, expire_on = {})
- write_generation
- generate_extras options, hash, expire_on
- end
-
- # Generate the query string with any extra keys in the hash and append
- # it to the given path, returning the new path.
- def append_query_string(path, hash, query_keys=nil)
- return nil unless path
- query_keys ||= extra_keys(hash)
- "#{path}#{build_query_string(hash, query_keys)}"
- end
-
- # Determine which keys in the given hash are "extra". Extra keys are
- # those that were not used to generate a particular route. The extra
- # keys also do not include those recalled from the prior request, nor
- # do they include any keys that were implied in the route (like a
- # <tt>:controller</tt> that is required, but not explicitly used in the
- # text of the route.)
- def extra_keys(hash, recall={})
- (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
- end
-
# Build a query string from the keys of the given hash. If +only_keys+
# is given (as an array), only the keys indicated will be used to build
# the query string. The query string will correctly build array parameter
@@ -161,12 +52,6 @@ module ActionController
elements.empty? ? '' : "?#{elements.sort * '&'}"
end
- # Write the real recognition implementation and then resend the message.
- def recognize(path, environment={})
- write_recognition
- recognize path, environment
- end
-
# A route's parameter shell contains parameter values that are not in the
# route's path, but should be placed in the recognized hash.
#
@@ -186,7 +71,7 @@ module ActionController
# includes keys that appear inside the path, and keys that have requirements
# placed upon them.
def significant_keys
- @significant_keys ||= returning [] do |sk|
+ @significant_keys ||= returning([]) do |sk|
segments.each { |segment| sk << segment.key if segment.respond_to? :key }
sk.concat requirements.keys
sk.uniq!
@@ -209,12 +94,7 @@ module ActionController
end
def matches_controller_and_action?(controller, action)
- unless defined? @matching_prepared
- @controller_requirement = requirement_for(:controller)
- @action_requirement = requirement_for(:action)
- @matching_prepared = true
- end
-
+ prepare_matching!
(@controller_requirement.nil? || @controller_requirement === controller) &&
(@action_requirement.nil? || @action_requirement === action)
end
@@ -226,15 +106,150 @@ module ActionController
end
end
- protected
- def requirement_for(key)
- return requirements[key] if requirements.key? key
- segments.each do |segment|
- return segment.regexp if segment.respond_to?(:key) && segment.key == key
+ # TODO: Route should be prepared and frozen on initialize
+ def freeze
+ unless frozen?
+ write_generation!
+ write_recognition!
+ prepare_matching!
+
+ parameter_shell
+ significant_keys
+ defaults
+ to_s
end
- nil
+
+ super
end
+ private
+ def requirement_for(key)
+ return requirements[key] if requirements.key? key
+ segments.each do |segment|
+ return segment.regexp if segment.respond_to?(:key) && segment.key == key
+ end
+ nil
+ end
+
+ # Write and compile a +generate+ method for this Route.
+ def write_generation!
+ # Build the main body of the generation
+ body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
+
+ # If we have conditions that must be tested first, nest the body inside an if
+ body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
+ args = "options, hash, expire_on = {}"
+
+ # Nest the body inside of a def block, and then compile it.
+ raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
+
+ # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
+ # are the same as the keys that were recalled from the previous request. Thus,
+ # we can use the expire_on.keys to determine which keys ought to be used to build
+ # the query string. (Never use keys from the recalled request when building the
+ # query string.)
+
+ method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
+
+ method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
+ raw_method
+ end
+
+ # Build several lines of code that extract values from the options hash. If any
+ # of the values are missing or rejected then a return will be executed.
+ def generation_extraction
+ segments.collect do |segment|
+ segment.extraction_code
+ end.compact * "\n"
+ end
+
+ # Produce a condition expression that will check the requirements of this route
+ # upon generation.
+ def generation_requirements
+ requirement_conditions = requirements.collect do |key, req|
+ if req.is_a? Regexp
+ value_regexp = Regexp.new "\\A#{req.to_s}\\Z"
+ "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
+ else
+ "hash[:#{key}] == #{req.inspect}"
+ end
+ end
+ requirement_conditions * ' && ' unless requirement_conditions.empty?
+ end
+
+ def generation_structure
+ segments.last.string_structure segments[0..-2]
+ end
+
+ # Write and compile a +recognize+ method for this Route.
+ def write_recognition!
+ # Create an if structure to extract the params from a match if it occurs.
+ body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
+ body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
+
+ # Build the method declaration and compile it
+ method_decl = "def recognize(path, env = {})\n#{body}\nend"
+ instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
+ method_decl
+ end
+
+ # Plugins may override this method to add other conditions, like checks on
+ # host, subdomain, and so forth. Note that changes here only affect route
+ # recognition, not generation.
+ def recognition_conditions
+ result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
+ result << "conditions[:method] === env[:method]" if conditions[:method]
+ result
+ end
+
+ # Build the regular expression pattern that will match this route.
+ def recognition_pattern(wrap = true)
+ pattern = ''
+ segments.reverse_each do |segment|
+ pattern = segment.build_pattern pattern
+ end
+ wrap ? ("\\A" + pattern + "\\Z") : pattern
+ end
+
+ # Write the code to extract the parameters from a matched route.
+ def recognition_extraction
+ next_capture = 1
+ extraction = segments.collect do |segment|
+ x = segment.match_extraction(next_capture)
+ next_capture += Regexp.new(segment.regexp_chunk).number_of_captures
+ x
+ end
+ extraction.compact
+ end
+
+ # Generate the query string with any extra keys in the hash and append
+ # it to the given path, returning the new path.
+ def append_query_string(path, hash, query_keys = nil)
+ return nil unless path
+ query_keys ||= extra_keys(hash)
+ "#{path}#{build_query_string(hash, query_keys)}"
+ end
+
+ # Determine which keys in the given hash are "extra". Extra keys are
+ # those that were not used to generate a particular route. The extra
+ # keys also do not include those recalled from the prior request, nor
+ # do they include any keys that were implied in the route (like a
+ # <tt>:controller</tt> that is required, but not explicitly used in the
+ # text of the route.)
+ def extra_keys(hash, recall = {})
+ (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
+ end
+
+ def prepare_matching!
+ unless defined? @matching_prepared
+ @controller_requirement = requirement_for(:controller)
+ @action_requirement = requirement_for(:action)
+ @matching_prepared = true
+ end
+ end
end
end
end
diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb
index 5bc13cf268..8dfc22f94f 100644
--- a/actionpack/lib/action_controller/routing/route_set.rb
+++ b/actionpack/lib/action_controller/routing/route_set.rb
@@ -1,6 +1,6 @@
module ActionController
module Routing
- class RouteSet #:nodoc:
+ class RouteSet #:nodoc:
# Mapper instances are used to build routes. The object passed to the draw
# block in config/routes.rb is a Mapper instance.
#
@@ -194,6 +194,8 @@ module ActionController
def initialize
self.routes = []
self.named_routes = NamedRouteCollection.new
+
+ write_recognize_optimized!
end
# Subclasses and plugins may override this method to specify a different
@@ -231,7 +233,6 @@ module ActionController
Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
clear!
load_routes!
- install_helpers
end
# reload! will always force a reload whereas load checks the timestamp first
@@ -432,4 +433,4 @@ module ActionController
end
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_controller/routing/routing_ext.rb b/actionpack/lib/action_controller/routing/routing_ext.rb
index 2ad20ee699..5f4ba90d0c 100644
--- a/actionpack/lib/action_controller/routing/routing_ext.rb
+++ b/actionpack/lib/action_controller/routing/routing_ext.rb
@@ -1,4 +1,3 @@
-
class Object
def to_param
to_s
diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb
index f0ad066bad..9d4b740a44 100644
--- a/actionpack/lib/action_controller/routing/segments.rb
+++ b/actionpack/lib/action_controller/routing/segments.rb
@@ -2,13 +2,15 @@ module ActionController
module Routing
class Segment #:nodoc:
RESERVED_PCHAR = ':@&=+$,;'
- UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze
+ SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
+ UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
+ # TODO: Convert :is_optional accessor to read only
attr_accessor :is_optional
alias_method :optional?, :is_optional
def initialize
- self.is_optional = false
+ @is_optional = false
end
def extraction_code
@@ -63,12 +65,14 @@ module ActionController
end
class StaticSegment < Segment #:nodoc:
- attr_accessor :value, :raw
+ attr_reader :value, :raw
alias_method :raw?, :raw
- def initialize(value = nil)
+ def initialize(value = nil, options = {})
super()
- self.value = value
+ @value = value
+ @raw = options[:raw] if options.key?(:raw)
+ @is_optional = options[:optional] if options.key?(:optional)
end
def interpolation_chunk
@@ -97,10 +101,8 @@ module ActionController
end
class DividerSegment < StaticSegment #:nodoc:
- def initialize(value = nil)
- super(value)
- self.raw = true
- self.is_optional = true
+ def initialize(value = nil, options = {})
+ super(value, {:raw => true, :optional => true}.merge(options))
end
def optionality_implied?
@@ -109,13 +111,17 @@ module ActionController
end
class DynamicSegment < Segment #:nodoc:
- attr_accessor :key, :default, :regexp
+ attr_reader :key
+
+ # TODO: Convert these accessors to read only
+ attr_accessor :default, :regexp
def initialize(key = nil, options = {})
super()
- self.key = key
- self.default = options[:default] if options.key? :default
- self.is_optional = true if options[:optional] || options.key?(:default)
+ @key = key
+ @default = options[:default] if options.key?(:default)
+ @regexp = options[:regexp] if options.key?(:regexp)
+ @is_optional = true if options[:optional] || options.key?(:default)
end
def to_s
@@ -130,6 +136,7 @@ module ActionController
def extract_value
"#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}"
end
+
def value_check
if default # Then we know it won't be nil
"#{value_regexp.inspect} =~ #{local_name}" if regexp
@@ -141,6 +148,7 @@ module ActionController
"#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}"
end
end
+
def expiry_statement
"expired, hash = true, options if !expired && expire_on[:#{key}]"
end
@@ -175,7 +183,7 @@ module ActionController
end
def regexp_chunk
- if regexp
+ if regexp
if regexp_has_modifiers?
"(#{regexp.to_s})"
else
@@ -214,7 +222,6 @@ module ActionController
def regexp_has_modifiers?
regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0
end
-
end
class ControllerSegment < DynamicSegment #:nodoc:
diff --git a/actionpack/lib/action_controller/session/cookie_store.rb b/actionpack/lib/action_controller/session/cookie_store.rb
index b477c1f7da..5bf7503f04 100644
--- a/actionpack/lib/action_controller/session/cookie_store.rb
+++ b/actionpack/lib/action_controller/session/cookie_store.rb
@@ -129,7 +129,7 @@ class CGI::Session::CookieStore
private
# Marshal a session hash into safe cookie data. Include an integrity hash.
def marshal(session)
- data = ActiveSupport::Base64.encode64(Marshal.dump(session)).chop
+ data = ActiveSupport::Base64.encode64s(Marshal.dump(session))
"#{data}--#{generate_digest(data)}"
end
diff --git a/actionpack/lib/action_controller/session/drb_server.rb b/actionpack/lib/action_controller/session/drb_server.rb
index 6f90db6747..2caa27f62a 100644..100755
--- a/actionpack/lib/action_controller/session/drb_server.rb
+++ b/actionpack/lib/action_controller/session/drb_server.rb
@@ -1,8 +1,8 @@
-#!/usr/local/bin/ruby -w
-
-# This is a really simple session storage daemon, basically just a hash,
+#!/usr/bin/env ruby
+
+# This is a really simple session storage daemon, basically just a hash,
# which is enabled for DRb access.
-
+
require 'drb'
session_hash = Hash.new
@@ -14,13 +14,13 @@ class <<session_hash
super(key, value)
end
end
-
+
def [](key)
@mutex.synchronize do
super(key)
end
end
-
+
def delete(key)
@mutex.synchronize do
super(key)
@@ -29,4 +29,4 @@ class <<session_hash
end
DRb.start_service('druby://127.0.0.1:9192', session_hash)
-DRb.thread.join \ No newline at end of file
+DRb.thread.join
diff --git a/actionpack/lib/action_controller/streaming.rb b/actionpack/lib/action_controller/streaming.rb
index 186e0e5531..333fb61b45 100644
--- a/actionpack/lib/action_controller/streaming.rb
+++ b/actionpack/lib/action_controller/streaming.rb
@@ -12,19 +12,21 @@ module ActionController #:nodoc:
X_SENDFILE_HEADER = 'X-Sendfile'.freeze
protected
- # Sends the file by streaming it 4096 bytes at a time. This way the
- # whole file doesn't need to be read into memory at once. This makes
- # it feasible to send even large files.
+ # Sends the file, by default streaming it 4096 bytes at a time. This way the
+ # whole file doesn't need to be read into memory at once. This makes it
+ # feasible to send even large files. You can optionally turn off streaming
+ # and send the whole file at once.
#
- # Be careful to sanitize the path parameter if it coming from a web
+ # Be careful to sanitize the path parameter if it is coming from a web
# page. <tt>send_file(params[:path])</tt> allows a malicious user to
# download any file on your server.
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# Defaults to <tt>File.basename(path)</tt>.
- # * <tt>:type</tt> - specifies an HTTP content type.
- # Defaults to 'application/octet-stream'.
+ # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
+ # * <tt>:length</tt> - used to manually override the length (in bytes) of the content that
+ # is going to be sent to the client. Defaults to <tt>File.size(path)</tt>.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:stream</tt> - whether to send the file to the user agent as it is read (+true+)
@@ -35,6 +37,12 @@ module ActionController #:nodoc:
# * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from
# the URL, which is necessary for i18n filenames on certain browsers
# (setting <tt>:filename</tt> overrides this option).
+ # * <tt>:x_sendfile</tt> - uses X-Sendfile to send the file when set to +true+. This is currently
+ # only available with Lighttpd/Apache2 and specific modules installed and activated. Since this
+ # uses the web server to send the file, this may lower memory consumption on your server and
+ # it will not block your application for further requests.
+ # See http://blog.lighttpd.net/articles/2006/07/02/x-sendfile and
+ # http://tn123.ath.cx/mod_xsendfile/ for details. Defaults to +false+.
#
# The default Content-Type and Content-Disposition headers are
# set to download arbitrary binary files in as many browsers as
@@ -99,8 +107,7 @@ module ActionController #:nodoc:
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
- # * <tt>:type</tt> - specifies an HTTP content type.
- # Defaults to 'application/octet-stream'.
+ # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
diff --git a/actionpack/lib/action_controller/templates/rescues/diagnostics.erb b/actionpack/lib/action_controller/templates/rescues/diagnostics.erb
index 385c6c1b09..b5483eccae 100644
--- a/actionpack/lib/action_controller/templates/rescues/diagnostics.erb
+++ b/actionpack/lib/action_controller/templates/rescues/diagnostics.erb
@@ -6,6 +6,6 @@
</h1>
<pre><%=h @exception.clean_message %></pre>
-<%= render(:file => @rescues_path + "/_trace.erb", :use_full_path => false) %>
+<%= render(:file => @rescues_path + "/_trace.erb") %>
-<%= render(:file => @rescues_path + "/_request_and_response.erb", :use_full_path => false) %>
+<%= render(:file => @rescues_path + "/_request_and_response.erb") %>
diff --git a/actionpack/lib/action_controller/templates/rescues/template_error.erb b/actionpack/lib/action_controller/templates/rescues/template_error.erb
index 4aecc68d18..76fa3df89d 100644
--- a/actionpack/lib/action_controller/templates/rescues/template_error.erb
+++ b/actionpack/lib/action_controller/templates/rescues/template_error.erb
@@ -15,7 +15,7 @@
<% @real_exception = @exception
@exception = @exception.original_exception || @exception %>
-<%= render(:file => @rescues_path + "/_trace.erb", :use_full_path => false) %>
+<%= render(:file => @rescues_path + "/_trace.erb") %>
<% @exception = @real_exception %>
-<%= render(:file => @rescues_path + "/_request_and_response.erb", :use_full_path => false) %>
+<%= render(:file => @rescues_path + "/_request_and_response.erb") %>
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 77c6f26eac..3e66947d5f 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -15,6 +15,65 @@ module ActionController
end
end
+ # Superclass for ActionController functional tests. Functional tests allow you to
+ # test a single controller action per test method. This should not be confused with
+ # integration tests (see ActionController::IntegrationTest), which are more like
+ # "stories" that can involve multiple controllers and mutliple actions (i.e. multiple
+ # different HTTP requests).
+ #
+ # == Basic example
+ #
+ # Functional tests are written as follows:
+ # 1. First, one uses the +get+, +post+, +put+, +delete+ or +head+ method to simulate
+ # an HTTP request.
+ # 2. Then, one asserts whether the current state is as expected. "State" can be anything:
+ # the controller's HTTP response, the database contents, etc.
+ #
+ # For example:
+ #
+ # class BooksControllerTest < ActionController::TestCase
+ # def test_create
+ # # Simulate a POST response with the given HTTP parameters.
+ # post(:create, :book => { :title => "Love Hina" })
+ #
+ # # Assert that the controller tried to redirect us to
+ # # the created book's URI.
+ # assert_response :found
+ #
+ # # Assert that the controller really put the book in the database.
+ # assert_not_nil Book.find_by_title("Love Hina")
+ # end
+ # end
+ #
+ # == Special instance variables
+ #
+ # ActionController::TestCase will also automatically provide the following instance
+ # variables for use in the tests:
+ #
+ # <b>@controller</b>::
+ # The controller instance that will be tested.
+ # <b>@request</b>::
+ # An ActionController::TestRequest, representing the current HTTP
+ # request. You can modify this object before sending the HTTP request. For example,
+ # you might want to set some session properties before sending a GET request.
+ # <b>@response</b>::
+ # An ActionController::TestResponse object, representing the response
+ # of the last HTTP response. In the above example, <tt>@response</tt> becomes valid
+ # after calling +post+. If the various assert methods are not sufficient, then you
+ # may use this object to inspect the HTTP response in detail.
+ #
+ # (Earlier versions of Rails required each functional test to subclass
+ # Test::Unit::TestCase and define @controller, @request, @response in +setup+.)
+ #
+ # == Controller is automatically inferred
+ #
+ # ActionController::TestCase will automatically infer the controller under test
+ # from the test class name. If the controller cannot be inferred from the test
+ # class name, you can explicity set it with +tests+.
+ #
+ # class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
+ # tests WidgetController
+ # end
class TestCase < ActiveSupport::TestCase
# When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline
# (bystepping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular
@@ -41,6 +100,8 @@ module ActionController
@@controller_class = nil
class << self
+ # Sets the controller class name. Useful if the name can't be inferred from test class.
+ # Expects +controller_class+ as a constant. Example: <tt>tests WidgetController</tt>.
def tests(controller_class)
self.controller_class = controller_class
end
@@ -80,4 +141,4 @@ module ActionController
@request.remote_addr = '208.77.188.166' # example.com
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb
index a6e0c98936..c6b1470070 100644
--- a/actionpack/lib/action_controller/test_process.rb
+++ b/actionpack/lib/action_controller/test_process.rb
@@ -23,7 +23,7 @@ module ActionController #:nodoc:
class TestRequest < AbstractRequest #:nodoc:
attr_accessor :cookies, :session_options
- attr_accessor :query_parameters, :request_parameters, :path, :session, :env
+ attr_accessor :query_parameters, :request_parameters, :path, :session
attr_accessor :host, :user_agent
def initialize(query_parameters = nil, request_parameters = nil, session = nil)
@@ -42,7 +42,7 @@ module ActionController #:nodoc:
end
# Wraps raw_post in a StringIO.
- def body
+ def body_stream #:nodoc:
StringIO.new(raw_post)
end
@@ -54,7 +54,7 @@ module ActionController #:nodoc:
def port=(number)
@env["SERVER_PORT"] = number.to_i
- @port_as_int = nil
+ port(true)
end
def action=(action_name)
@@ -68,6 +68,8 @@ module ActionController #:nodoc:
@env["REQUEST_URI"] = value
@request_uri = nil
@path = nil
+ request_uri(true)
+ path(true)
end
def request_uri=(uri)
@@ -77,21 +79,26 @@ module ActionController #:nodoc:
def accept=(mime_types)
@env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",")
+ accepts(true)
end
- def remote_addr=(addr)
- @env['REMOTE_ADDR'] = addr
+ def if_modified_since=(last_modified)
+ @env["HTTP_IF_MODIFIED_SINCE"] = last_modified
end
- def remote_addr
- @env['REMOTE_ADDR']
+ def if_none_match=(etag)
+ @env["HTTP_IF_NONE_MATCH"] = etag
end
- def request_uri
+ def remote_addr=(addr)
+ @env['REMOTE_ADDR'] = addr
+ end
+
+ def request_uri(*args)
@request_uri || super
end
- def path
+ def path(*args)
@path || super
end
@@ -113,17 +120,13 @@ module ActionController #:nodoc:
end
end
@parameters = nil # reset TestRequest#parameters to use the new path_parameters
- end
-
+ end
+
def recycle!
self.request_parameters = {}
self.query_parameters = {}
self.path_parameters = {}
- @request_method, @accepts, @content_type = nil, nil, nil
- end
-
- def referer
- @env["HTTP_REFERER"]
+ unmemoize_all
end
private
@@ -135,7 +138,7 @@ module ActionController #:nodoc:
@host = "test.host"
@request_uri = "/"
@user_agent = "Rails Testing"
- self.remote_addr = "0.0.0.0"
+ self.remote_addr = "0.0.0.0"
@env["SERVER_PORT"] = 80
@env['REQUEST_METHOD'] = "GET"
end
@@ -157,21 +160,21 @@ module ActionController #:nodoc:
module TestResponseBehavior #:nodoc:
# The response code of the request
def response_code
- headers['Status'][0,3].to_i rescue 0
+ status[0,3].to_i rescue 0
end
-
+
# Returns a String to ensure compatibility with Net::HTTPResponse
def code
- headers['Status'].to_s.split(' ')[0]
+ status.to_s.split(' ')[0]
end
def message
- headers['Status'].to_s.split(' ',2)[1]
+ status.to_s.split(' ',2)[1]
end
# Was the response successful?
def success?
- response_code == 200
+ (200..299).include?(response_code)
end
# Was the URL not found?
@@ -211,7 +214,7 @@ module ActionController #:nodoc:
template._first_render
end
- # A shortcut to the flash. Returns an empyt hash if no session flash exists.
+ # A shortcut to the flash. Returns an empty hash if no session flash exists.
def flash
session['flash'] || {}
end
@@ -243,11 +246,11 @@ module ActionController #:nodoc:
# Does the specified template object exist?
def has_template_object?(name=nil)
- !template_objects[name].nil?
+ !template_objects[name].nil?
end
# Returns the response cookies, converted to a Hash of (name => CGI::Cookie) pairs
- #
+ #
# assert_equal ['AuthorOfNewPage'], r.cookies['author'].value
def cookies
headers['cookie'].inject({}) { |hash, cookie| hash[cookie.name] = cookie; hash }
@@ -266,7 +269,13 @@ module ActionController #:nodoc:
end
end
- class TestResponse < AbstractResponse #:nodoc:
+ # Integration test methods such as ActionController::Integration::Session#get
+ # and ActionController::Integration::Session#post return objects of class
+ # TestResponse, which represent the HTTP response results of the requested
+ # controller actions.
+ #
+ # See AbstractResponse for more information on controller response objects.
+ class TestResponse < AbstractResponse
include TestResponseBehavior
end
@@ -313,7 +322,7 @@ module ActionController #:nodoc:
#
# Usage example, within a functional test:
# post :change_avatar, :avatar => ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + '/files/spongebob.png', 'image/png')
- #
+ #
# Pass a true third parameter to ensure the uploaded file is opened in binary mode (only required for Windows):
# post :change_avatar, :avatar => ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + '/files/spongebob.png', 'image/png', :binary)
require 'tempfile'
@@ -348,6 +357,7 @@ module ActionController #:nodoc:
module TestProcess
def self.included(base)
# execute the request simulating a specific HTTP method and set/volley the response
+ # TODO: this should be un-DRY'ed for the sake of API documentation.
%w( get post put delete head ).each do |method|
base.class_eval <<-EOV, __FILE__, __LINE__
def #{method}(action, parameters = nil, session = nil, flash = nil)
@@ -393,13 +403,13 @@ module ActionController #:nodoc:
end
alias xhr :xml_http_request
- def assigns(key = nil)
- if key.nil?
- @response.template.assigns
- else
- @response.template.assigns[key.to_s]
- end
- end
+ def assigns(key = nil)
+ if key.nil?
+ @response.template.assigns
+ else
+ @response.template.assigns[key.to_s]
+ end
+ end
def session
@response.session
@@ -441,10 +451,13 @@ module ActionController #:nodoc:
end
def method_missing(selector, *args)
- return @controller.send!(selector, *args) if ActionController::Routing::Routes.named_routes.helpers.include?(selector)
- return super
+ if ActionController::Routing::Routes.named_routes.helpers.include?(selector)
+ @controller.send(selector, *args)
+ else
+ super
+ end
end
-
+
# Shortcut for <tt>ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + path, type)</tt>:
#
# post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png')
@@ -455,7 +468,7 @@ module ActionController #:nodoc:
# post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png', :binary)
def fixture_file_upload(path, mime_type = nil, binary = false)
ActionController::TestUploadedFile.new(
- Test::Unit::TestCase.respond_to?(:fixture_path) ? Test::Unit::TestCase.fixture_path + path : path,
+ Test::Unit::TestCase.respond_to?(:fixture_path) ? Test::Unit::TestCase.fixture_path + path : path,
mime_type,
binary
)
@@ -463,7 +476,7 @@ module ActionController #:nodoc:
# A helper to make it easier to test different route configurations.
# This method temporarily replaces ActionController::Routing::Routes
- # with a new RouteSet instance.
+ # with a new RouteSet instance.
#
# The new instance is yielded to the passed block. Typically the block
# will create some routes using <tt>map.draw { map.connect ... }</tt>:
diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb
index 3a38f23396..d86e2db67d 100644
--- a/actionpack/lib/action_controller/url_rewriter.rb
+++ b/actionpack/lib/action_controller/url_rewriter.rb
@@ -1,19 +1,96 @@
module ActionController
- # Write URLs from arbitrary places in your codebase, such as your mailers.
+ # In <b>routes.rb</b> one defines URL-to-controller mappings, but the reverse
+ # is also possible: an URL can be generated from one of your routing definitions.
+ # URL generation functionality is centralized in this module.
#
- # Example:
+ # See ActionController::Routing and ActionController::Resources for general
+ # information about routing and routes.rb.
#
- # class MyMailer
- # include ActionController::UrlWriter
- # default_url_options[:host] = 'www.basecamphq.com'
+ # <b>Tip:</b> If you need to generate URLs from your models or some other place,
+ # then ActionController::UrlWriter is what you're looking for. Read on for
+ # an introduction.
#
- # def signup_url(token)
- # url_for(:controller => 'signup', action => 'index', :token => token)
+ # == URL generation from parameters
+ #
+ # As you may know, some functions - such as ActionController::Base#url_for
+ # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set
+ # of parameters. For example, you've probably had the chance to write code
+ # like this in one of your views:
+ #
+ # <%= link_to('Click here', :controller => 'users',
+ # :action => 'new', :message => 'Welcome!') %>
+ #
+ # #=> Generates a link to: /users/new?message=Welcome%21
+ #
+ # link_to, and all other functions that require URL generation functionality,
+ # actually use ActionController::UrlWriter under the hood. And in particular,
+ # they use the ActionController::UrlWriter#url_for method. One can generate
+ # the same path as the above example by using the following code:
+ #
+ # include UrlWriter
+ # url_for(:controller => 'users',
+ # :action => 'new',
+ # :message => 'Welcome!',
+ # :only_path => true)
+ # # => "/users/new?message=Welcome%21"
+ #
+ # Notice the <tt>:only_path => true</tt> part. This is because UrlWriter has no
+ # information about the website hostname that your Rails app is serving. So if you
+ # want to include the hostname as well, then you must also pass the <tt>:host</tt>
+ # argument:
+ #
+ # include UrlWriter
+ # url_for(:controller => 'users',
+ # :action => 'new',
+ # :message => 'Welcome!',
+ # :host => 'www.example.com') # Changed this.
+ # # => "http://www.example.com/users/new?message=Welcome%21"
+ #
+ # By default, all controllers and views have access to a special version of url_for,
+ # that already knows what the current hostname is. So if you use url_for in your
+ # controllers or your views, then you don't need to explicitly pass the <tt>:host</tt>
+ # argument.
+ #
+ # For convenience reasons, mailers provide a shortcut for ActionController::UrlWriter#url_for.
+ # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlWriter#url_for'
+ # in full. However, mailers don't have hostname information, and what's why you'll still
+ # have to specify the <tt>:host</tt> argument when generating URLs in mailers.
+ #
+ #
+ # == URL generation for named routes
+ #
+ # UrlWriter also allows one to access methods that have been auto-generated from
+ # named routes. For example, suppose that you have a 'users' resource in your
+ # <b>routes.rb</b>:
+ #
+ # map.resources :users
+ #
+ # This generates, among other things, the method <tt>users_path</tt>. By default,
+ # this method is accessible from your controllers, views and mailers. If you need
+ # to access this auto-generated method from other places (such as a model), then
+ # you can do that in two ways.
+ #
+ # The first way is to include ActionController::UrlWriter in your class:
+ #
+ # class User < ActiveRecord::Base
+ # include ActionController::UrlWriter # !!!
+ #
+ # def name=(value)
+ # write_attribute('name', value)
+ # write_attribute('base_uri', users_path) # !!!
# end
- # end
+ # end
#
- # In addition to providing +url_for+, named routes are also accessible after
- # including UrlWriter.
+ # The second way is to access them through ActionController::UrlWriter.
+ # The autogenerated named routes methods are available as class methods:
+ #
+ # class User < ActiveRecord::Base
+ # def name=(value)
+ # write_attribute('name', value)
+ # path = ActionController::UrlWriter.users_path # !!!
+ # write_attribute('base_uri', path) # !!!
+ # end
+ # end
module UrlWriter
# The default options for urls written by this writer. Typically a <tt>:host</tt>
# pair is provided.
@@ -37,7 +114,7 @@ module ActionController
# * <tt>:port</tt> - Optionally specify the port to connect to.
# * <tt>:anchor</tt> - An anchor name to be appended to the path.
# * <tt>:skip_relative_url_root</tt> - If true, the url is not constructed using the
- # +relative_url_root+ set in ActionController::AbstractRequest.relative_url_root.
+ # +relative_url_root+ set in ActionController::Base.relative_url_root.
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
#
# Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to
@@ -67,7 +144,7 @@ module ActionController
[:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) }
end
trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash)
- url << ActionController::AbstractRequest.relative_url_root.to_s unless options[:skip_relative_url_root]
+ url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor]
generated = Routing::Routes.generate(options, {})
url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated)
@@ -108,7 +185,7 @@ module ActionController
end
path = rewrite_path(options)
- rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root]
+ rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
rewritten_url << "##{options[:anchor]}" if options[:anchor]
diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb
index 1a3c770254..376bb87409 100644
--- a/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb
+++ b/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb
@@ -64,7 +64,7 @@ module HTML
#
# When using a combination of the above, the element name comes first
# followed by identifier, class names, attributes, pseudo classes and
- # negation in any order. Do not seprate these parts with spaces!
+ # negation in any order. Do not separate these parts with spaces!
# Space separation is used for descendant selectors.
#
# For example:
@@ -158,7 +158,7 @@ module HTML
# * <tt>:not(selector)</tt> -- Match the element only if the element does not
# match the simple selector.
#
- # As you can see, <tt>:nth-child<tt> pseudo class and its varient can get quite
+ # As you can see, <tt>:nth-child<tt> pseudo class and its variant can get quite
# tricky and the CSS specification doesn't do a much better job explaining it.
# But after reading the examples and trying a few combinations, it's easy to
# figure out.
diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb
index 9ab615c7a5..3590ab6d49 100644
--- a/actionpack/lib/action_view.rb
+++ b/actionpack/lib/action_view.rb
@@ -21,6 +21,15 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
+begin
+ require 'active_support'
+rescue LoadError
+ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
+ if File.directory?(activesupport_path)
+ $:.unshift activesupport_path
+ require 'active_support'
+ end
+end
require 'action_view/template_handlers'
require 'action_view/renderable'
@@ -34,10 +43,13 @@ require 'action_view/base'
require 'action_view/partials'
require 'action_view/template_error'
+I18n.backend.populate do
+ I18n.load_translations "#{File.dirname(__FILE__)}/action_view/locale/en-US.yml"
+end
+
+require 'action_view/helpers'
+
ActionView::Base.class_eval do
include ActionView::Partials
-
- ActionView::Base.helper_modules.each do |helper_module|
- include helper_module
- end
+ include ActionView::Helpers
end
diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb
index 04e8d3a358..d174c784f3 100644
--- a/actionpack/lib/action_view/base.rb
+++ b/actionpack/lib/action_view/base.rb
@@ -158,6 +158,7 @@ module ActionView #:nodoc:
# See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details.
class Base
include ERB::Util
+ extend ActiveSupport::Memoizable
attr_accessor :base_path, :assigns, :template_extension
attr_accessor :controller
@@ -169,15 +170,19 @@ module ActionView #:nodoc:
class << self
delegate :erb_trim_mode=, :to => 'ActionView::TemplateHandlers::ERB'
+ delegate :logger, :to => 'ActionController::Base'
end
- # Specify whether templates should be cached. Otherwise the file we be read everytime it is accessed.
- @@cache_template_loading = false
- cattr_accessor :cache_template_loading
+ def self.cache_template_loading=(*args)
+ ActiveSupport::Deprecation.warn(
+ "config.action_view.cache_template_loading option has been deprecated" +
+ "and has no effect. Please remove it from your config files.", caller)
+ end
def self.cache_template_extensions=(*args)
- ActiveSupport::Deprecation.warn("config.action_view.cache_template_extensions option has been deprecated and has no affect. " <<
- "Please remove it from your config files.", caller)
+ ActiveSupport::Deprecation.warn(
+ "config.action_view.cache_template_extensions option has been" +
+ "deprecated and has no effect. Please remove it from your config files.", caller)
end
# Specify whether RJS responses should be wrapped in a try/catch block
@@ -199,23 +204,6 @@ module ActionView #:nodoc:
end
include CompiledTemplates
- # Cache public asset paths
- cattr_reader :computed_public_paths
- @@computed_public_paths = {}
-
- def self.helper_modules #:nodoc:
- helpers = []
- Dir.entries(File.expand_path("#{File.dirname(__FILE__)}/helpers")).sort.each do |file|
- next unless file =~ /^([a-z][a-z_]*_helper).rb$/
- require "action_view/helpers/#{$1}"
- helper_module_name = $1.camelize
- if Helpers.const_defined?(helper_module_name)
- helpers << Helpers.const_get(helper_module_name)
- end
- end
- return helpers
- end
-
def self.process_view_paths(value)
ActionView::PathSet.new(Array(value))
end
@@ -239,7 +227,7 @@ module ActionView #:nodoc:
local_assigns ||= {}
if options.is_a?(String)
- render_file(options, nil, local_assigns)
+ render(:file => options, :locals => local_assigns)
elsif options == :update
update_page(&block)
elsif options.is_a?(Hash)
@@ -247,31 +235,34 @@ module ActionView #:nodoc:
if partial_layout = options.delete(:layout)
if block_given?
- wrap_content_for_layout capture(&block) do
+ begin
+ @_proc_for_layout = block
concat(render(options.merge(:partial => partial_layout)))
+ ensure
+ @_proc_for_layout = nil
end
else
- wrap_content_for_layout render(options) do
+ begin
+ original_content_for_layout, @content_for_layout = @content_for_layout, render(options)
render(options.merge(:partial => partial_layout))
+ ensure
+ @content_for_layout = original_content_for_layout
end
end
elsif options[:file]
- render_file(options[:file], nil, options[:locals])
- elsif options[:partial] && options[:collection]
- render_partial_collection(options[:partial], options[:collection], options[:spacer_template], options[:locals], options[:as])
+ if options[:use_full_path]
+ ActiveSupport::Deprecation.warn("use_full_path option has been deprecated and has no affect.", caller)
+ end
+
+ pick_template(options[:file]).render_template(self, options[:locals])
elsif options[:partial]
- render_partial(options[:partial], options[:object], options[:locals])
+ render_partial(options)
elsif options[:inline]
- render_inline(options[:inline], options[:locals], options[:type])
+ InlineTemplate.new(options[:inline], options[:type]).render(self, options[:locals])
end
end
end
- # Returns true is the file may be rendered implicitly.
- def file_public?(template_path)#:nodoc:
- template_path.split('/').last[0,1] != '_'
- end
-
# The format to be used when choosing between multiple templates with
# the same name but differing formats. See +Request#template_format+
# for more details.
@@ -301,6 +292,8 @@ module ActionView #:nodoc:
# # => 'users/legacy.rhtml'
#
def pick_template(template_path)
+ return template_path if template_path.respond_to?(:render)
+
path = template_path.sub(/^\//, '')
if m = path.match(/(.*)\.(\w+)$/)
template_file_name, template_file_extension = m[1], m[2]
@@ -321,54 +314,20 @@ module ActionView #:nodoc:
else
template = Template.new(template_path, view_paths)
- if self.class.warn_cache_misses && logger = ActionController::Base.logger
+ if self.class.warn_cache_misses && logger
logger.debug "[PERFORMANCE] Rendering a template that was " +
"not found in view path. Templates outside the view path are " +
- "not cached and result in expensive disk operations. Move this " +
- "file into #{view_paths.join(':')} or add the folder to your " +
+ "not cached and result in expensive disk operations. Move this " +
+ "file into #{view_paths.join(':')} or add the folder to your " +
"view path list"
end
template
end
end
+ memoize :pick_template
private
- # Renders the template present at <tt>template_path</tt>. The hash in <tt>local_assigns</tt>
- # is made available as local variables.
- def render_file(template_path, use_full_path = nil, local_assigns = {}) #:nodoc:
- unless use_full_path == nil
- ActiveSupport::Deprecation.warn("use_full_path option has been deprecated and has no affect.", caller)
- end
-
- if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && !template_path.include?("/")
- raise ActionViewError, <<-END_ERROR
- Due to changes in ActionMailer, you need to provide the mailer_name along with the template name.
-
- render "user_mailer/signup"
- render :file => "user_mailer/signup"
-
- If you are rendering a subtemplate, you must now use controller-like partial syntax:
-
- render :partial => 'signup' # no mailer_name necessary
- END_ERROR
- end
-
- template = pick_template(template_path)
- template.render_template(self, local_assigns)
- end
-
- def render_inline(text, local_assigns = {}, type = nil)
- InlineTemplate.new(text, type).render(self, local_assigns)
- end
-
- def wrap_content_for_layout(content)
- original_content_for_layout, @content_for_layout = @content_for_layout, content
- yield
- ensure
- @content_for_layout = original_content_for_layout
- end
-
# Evaluate the local assigns and pushes them to the view.
def evaluate_assigns
unless @assigns_added
@@ -382,9 +341,9 @@ module ActionView #:nodoc:
@assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end
- def execute(template, local_assigns = {})
- send(template.method(local_assigns), local_assigns) do |*names|
- instance_variable_get "@content_for_#{names.first || 'layout'}"
+ def set_controller_content_type(content_type)
+ if controller.respond_to?(:response)
+ controller.response.content_type ||= content_type
end
end
end
diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb
new file mode 100644
index 0000000000..05e1cf990a
--- /dev/null
+++ b/actionpack/lib/action_view/helpers.rb
@@ -0,0 +1,39 @@
+Dir.entries(File.expand_path("#{File.dirname(__FILE__)}/helpers")).sort.each do |file|
+ next unless file =~ /^([a-z][a-z_]*_helper).rb$/
+ require "action_view/helpers/#{$1}"
+end
+
+module ActionView #:nodoc:
+ module Helpers #:nodoc:
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ include SanitizeHelper::ClassMethods
+ end
+
+ include ActiveRecordHelper
+ include AssetTagHelper
+ include AtomFeedHelper
+ include BenchmarkHelper
+ include CacheHelper
+ include CaptureHelper
+ include DateHelper
+ include DebugHelper
+ include FormCountryHelper
+ include FormHelper
+ include FormOptionsHelper
+ include FormTagHelper
+ include NumberHelper
+ include PrototypeHelper
+ include RecordIdentificationHelper
+ include RecordTagHelper
+ include SanitizeHelper
+ include ScriptaculousHelper
+ include TagHelper
+ include TextHelper
+ include TranslationHelper
+ include UrlHelper
+ end
+end
diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb
index f3f204cc97..c339e10701 100644
--- a/actionpack/lib/action_view/helpers/active_record_helper.rb
+++ b/actionpack/lib/action_view/helpers/active_record_helper.rb
@@ -25,7 +25,7 @@ module ActionView
# Returns an entire form with all needed input tags for a specified Active Record object. For example, if <tt>@post</tt>
# has attributes named +title+ of type +VARCHAR+ and +body+ of type +TEXT+ then
#
- # form("post")
+ # form("post")
#
# would yield a form like the following (modulus formatting):
#
@@ -90,23 +90,41 @@ module ActionView
end
# Returns a string containing the error message attached to the +method+ on the +object+ if one exists.
- # This error message is wrapped in a <tt>DIV</tt> tag, which can be extended to include a +prepend_text+ and/or +append_text+
- # (to properly explain the error), and a +css_class+ to style it accordingly. +object+ should either be the name of an instance variable or
- # the actual object. As an example, let's say you have a model <tt>@post</tt> that has an error message on the +title+ attribute:
+ # This error message is wrapped in a <tt>DIV</tt> tag, which can be extended to include a <tt>:prepend_text</tt>
+ # and/or <tt>:append_text</tt> (to properly explain the error), and a <tt>:css_class</tt> to style it
+ # accordingly. +object+ should either be the name of an instance variable or the actual object. The method can be
+ # passed in either as a string or a symbol.
+ # As an example, let's say you have a model <tt>@post</tt> that has an error message on the +title+ attribute:
#
# <%= error_message_on "post", "title" %>
# # => <div class="formError">can't be empty</div>
#
- # <%= error_message_on @post, "title" %>
+ # <%= error_message_on @post, :title %>
# # => <div class="formError">can't be empty</div>
#
- # <%= error_message_on "post", "title", "Title simply ", " (or it won't work).", "inputError" %>
- # # => <div class="inputError">Title simply can't be empty (or it won't work).</div>
- def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError")
+ # <%= error_message_on "post", "title",
+ # :prepend_text => "Title simply ",
+ # :append_text => " (or it won't work).",
+ # :css_class => "inputError" %>
+ def error_message_on(object, method, *args)
+ options = args.extract_options!
+ unless args.empty?
+ ActiveSupport::Deprecation.warn('error_message_on takes an option hash instead of separate ' +
+ 'prepend_text, append_text, and css_class arguments', caller)
+
+ options[:prepend_text] = args[0] || ''
+ options[:append_text] = args[1] || ''
+ options[:css_class] = args[2] || 'formError'
+ end
+ options.reverse_merge!(:prepend_text => '', :append_text => '', :css_class => 'formError')
+
if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) &&
(errors = obj.errors.on(method))
- content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class)
- else
+ content_tag("div",
+ "#{options[:prepend_text]}#{errors.is_a?(Array) ? errors.first : errors}#{options[:append_text]}",
+ :class => options[:css_class]
+ )
+ else
''
end
end
@@ -133,7 +151,7 @@ module ActionView
#
# To specify the display for one object, you simply provide its name as a parameter.
# For example, for the <tt>@user</tt> model:
- #
+ #
# error_messages_for 'user'
#
# To specify more than one object, you simply list them; optionally, you can add an extra <tt>:object_name</tt> parameter, which
@@ -141,7 +159,7 @@ module ActionView
#
# error_messages_for 'user_common', 'user', :object_name => 'user'
#
- # If the objects cannot be located as instance variables, you can add an extra <tt>:object</tt> paremeter which gives the actual
+ # If the objects cannot be located as instance variables, you can add an extra <tt>:object</tt> parameter which gives the actual
# object (or array of objects to use):
#
# error_messages_for 'user', :object => @question.user
@@ -151,12 +169,14 @@ module ActionView
# instance yourself and set it up. View the source of this method to see how easy it is.
def error_messages_for(*params)
options = params.extract_options!.symbolize_keys
+
if object = options.delete(:object)
objects = [object].flatten
else
objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact
end
- count = objects.inject(0) {|sum, object| sum + object.errors.count }
+
+ count = objects.inject(0) {|sum, object| sum + object.errors.count }
unless count.zero?
html = {}
[:id, :class].each do |key|
@@ -168,16 +188,25 @@ module ActionView
end
end
options[:object_name] ||= params.first
- options[:header_message] = "#{pluralize(count, 'error')} prohibited this #{options[:object_name].to_s.gsub('_', ' ')} from being saved" unless options.include?(:header_message)
- options[:message] ||= 'There were problems with the following fields:' unless options.include?(:message)
- error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join
- contents = ''
- contents << content_tag(options[:header_tag] || :h2, options[:header_message]) unless options[:header_message].blank?
- contents << content_tag(:p, options[:message]) unless options[:message].blank?
- contents << content_tag(:ul, error_messages)
+ I18n.with_options :locale => options[:locale], :scope => [:activerecord, :errors, :template] do |locale|
+ header_message = if options.include?(:header_message)
+ options[:header_message]
+ else
+ object_name = options[:object_name].to_s.gsub('_', ' ')
+ object_name = I18n.t(object_name, :default => object_name, :scope => [:activerecord, :models], :count => 1)
+ locale.t :header, :count => count, :model => object_name
+ end
+ message = options.include?(:message) ? options[:message] : locale.t(:body)
+ error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join
- content_tag(:div, contents, html)
+ contents = ''
+ contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
+ contents << content_tag(:p, message) unless message.blank?
+ contents << content_tag(:ul, error_messages)
+
+ content_tag(:div, contents, html)
+ end
else
''
end
diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb
index bf13945844..623ed1e8df 100644
--- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb
@@ -5,12 +5,12 @@ require 'action_view/helpers/tag_helper'
module ActionView
module Helpers #:nodoc:
# This module provides methods for generating HTML that links views to assets such
- # as images, javascripts, stylesheets, and feeds. These methods do not verify
- # the assets exist before linking to them.
+ # as images, javascripts, stylesheets, and feeds. These methods do not verify
+ # the assets exist before linking to them.
#
# === Using asset hosts
# By default, Rails links to these assets on the current host in the public
- # folder, but you can direct Rails to link to assets from a dedicated assets server by
+ # folder, but you can direct Rails to link to assets from a dedicated assets server by
# setting ActionController::Base.asset_host in your <tt>config/environment.rb</tt>. For example,
# let's say your asset host is <tt>assets.example.com</tt>.
#
@@ -22,16 +22,16 @@ module ActionView
#
# This is useful since browsers typically open at most two connections to a single host,
# which means your assets often wait in single file for their turn to load. You can
- # alleviate this by using a <tt>%d</tt> wildcard in <tt>asset_host</tt> (for example, "assets%d.example.com")
+ # alleviate this by using a <tt>%d</tt> wildcard in <tt>asset_host</tt> (for example, "assets%d.example.com")
# to automatically distribute asset requests among four hosts (e.g., "assets0.example.com" through "assets3.example.com")
- # so browsers will open eight connections rather than two.
+ # so browsers will open eight connections rather than two.
#
# image_tag("rails.png")
# => <img src="http://assets0.example.com/images/rails.png" alt="Rails" />
# stylesheet_link_tag("application")
# => <link href="http://assets3.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
#
- # To do this, you can either setup 4 actual hosts, or you can use wildcard DNS to CNAME
+ # To do this, you can either setup 4 actual hosts, or you can use wildcard DNS to CNAME
# the wildcard to a single asset host. You can read more about setting up your DNS CNAME records from
# your ISP.
#
@@ -86,7 +86,7 @@ module ActionView
# asset far into the future, but still be able to instantly invalidate it by simply updating the file (and hence updating the timestamp,
# which then updates the URL as the timestamp is part of that, which in turn busts the cache).
#
- # It's the responsibility of the web server you use to set the far-future expiration date on cache assets that you need to take
+ # It's the responsibility of the web server you use to set the far-future expiration date on cache assets that you need to take
# advantage of this feature. Here's an example for Apache:
#
# # Asset Expiration
@@ -95,16 +95,17 @@ module ActionView
# ExpiresDefault "access plus 1 year"
# </FilesMatch>
#
- # Also note that in order for this to work, all your application servers must return the same timestamps. This means that they must
+ # Also note that in order for this to work, all your application servers must return the same timestamps. This means that they must
# have their clocks synchronized. If one of them drift out of sync, you'll see different timestamps at random and the cache won't
# work. Which means that the browser will request the same assets over and over again even thought they didn't change. You can use
- # something like Live HTTP Headers for Firefox to verify that the cache is indeed working (and that the assets are not being
+ # something like Live HTTP Headers for Firefox to verify that the cache is indeed working (and that the assets are not being
# requested over and over).
module AssetTagHelper
ASSETS_DIR = defined?(Rails.public_path) ? Rails.public_path : "public"
JAVASCRIPTS_DIR = "#{ASSETS_DIR}/javascripts"
STYLESHEETS_DIR = "#{ASSETS_DIR}/stylesheets"
-
+ JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'].map(&:to_s).freeze unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES)
+
# Returns a link tag that browsers and news readers can use to auto-detect
# an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or
# <tt>:atom</tt>. Control the link options in url_for format using the
@@ -154,10 +155,6 @@ module ActionView
end
alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
- JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'] unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES)
- @@javascript_expansions = { :defaults => JAVASCRIPT_DEFAULT_SOURCES.dup }
- @@stylesheet_expansions = {}
-
# Returns an html script tag for each of the +sources+ provided. You
# can pass in the filename (.js extension is optional) of javascript files
# that exist in your public/javascripts directory for inclusion into the
@@ -193,7 +190,7 @@ module ActionView
#
# * = The application.js file is only referenced if it exists
#
- # Though it's not really recommended practice, if you need to extend the default JavaScript set for any reason
+ # Though it's not really recommended practice, if you need to extend the default JavaScript set for any reason
# (e.g., you're going to be using a certain .js file in every action), then take a look at the register_javascript_include_default method.
#
# You can also include all javascripts in the javascripts directory using <tt>:all</tt> as the source:
@@ -218,7 +215,7 @@ module ActionView
# You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be
# compressed by gzip (leading to faster transfers). Caching will only happen if ActionController::Base.perform_caching
# is set to <tt>true</tt> (which is the case by default for the Rails production environment, but not for the development
- # environment).
+ # environment).
#
# ==== Examples
# javascript_include_tag :all, :cache => true # when ActionController::Base.perform_caching is false =>
@@ -259,6 +256,8 @@ module ActionView
end
end
+ @@javascript_expansions = { :defaults => JAVASCRIPT_DEFAULT_SOURCES.dup }
+
# Register one or more javascript files to be included when <tt>symbol</tt>
# is passed to <tt>javascript_include_tag</tt>. This method is typically intended
# to be called from plugin initialization to register javascript files
@@ -274,6 +273,8 @@ module ActionView
@@javascript_expansions.merge!(expansions)
end
+ @@stylesheet_expansions = {}
+
# Register one or more stylesheet files to be included when <tt>symbol</tt>
# is passed to <tt>stylesheet_link_tag</tt>. This method is typically intended
# to be called from plugin initialization to register stylesheet files
@@ -439,9 +440,9 @@ module ActionView
# <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
# image_tag("/icons/icon.gif", :class => "menu_icon") # =>
# <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
- # image_tag("mouse.png", :mouseover => "/images/mouse_over.png") # =>
+ # image_tag("mouse.png", :mouseover => "/images/mouse_over.png") # =>
# <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" />
- # image_tag("mouse.png", :mouseover => image_path("mouse_over.png")) # =>
+ # image_tag("mouse.png", :mouseover => image_path("mouse_over.png")) # =>
# <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" />
def image_tag(source, options = {})
options.symbolize_keys!
@@ -454,23 +455,15 @@ module ActionView
end
if mouseover = options.delete(:mouseover)
- options[:onmouseover] = "this.src='#{image_path(mouseover)}'"
- options[:onmouseout] = "this.src='#{image_path(options[:src])}'"
+ options[:onmouseover] = "this.src='#{image_path(mouseover)}'"
+ options[:onmouseout] = "this.src='#{image_path(options[:src])}'"
end
tag("img", options)
end
private
- def file_exist?(path)
- @@file_exist_cache ||= {}
- if !(@@file_exist_cache[path] ||= File.exist?(path))
- @@file_exist_cache[path] = true
- false
- else
- true
- end
- end
+ COMPUTED_PUBLIC_PATHS = ActiveSupport::Cache::MemoryStore.new.silence!
# Add the the extension +ext+ if not present. Return full URLs otherwise untouched.
# Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL
@@ -483,14 +476,14 @@ module ActionView
if has_request
[ @controller.request.protocol,
ActionController::Base.asset_host.to_s,
- @controller.request.relative_url_root,
+ ActionController::Base.relative_url_root,
dir, source, ext, include_host ].join
else
[ ActionController::Base.asset_host.to_s,
dir, source, ext, include_host ].join
end
- ActionView::Base.computed_public_paths[cache_key] ||=
+ source = COMPUTED_PUBLIC_PATHS.fetch(cache_key) do
begin
source += ".#{ext}" if ext && File.extname(source).blank? || File.exist?(File.join(ASSETS_DIR, dir, "#{source}.#{ext}"))
@@ -499,25 +492,27 @@ module ActionView
else
source = "/#{dir}/#{source}" unless source[0] == ?/
if has_request
- unless source =~ %r{^#{@controller.request.relative_url_root}/}
- source = "#{@controller.request.relative_url_root}#{source}"
+ unless source =~ %r{^#{ActionController::Base.relative_url_root}/}
+ source = "#{ActionController::Base.relative_url_root}#{source}"
end
end
- source = rewrite_asset_path(source)
- if include_host
- host = compute_asset_host(source)
+ rewrite_asset_path(source)
+ end
+ end
+ end
- if has_request && !host.blank? && host !~ %r{^[-a-z]+://}
- host = "#{@controller.request.protocol}#{host}"
- end
+ if include_host && source !~ %r{^[-a-z]+://}
+ host = compute_asset_host(source)
- "#{host}#{source}"
- else
- source
- end
- end
+ if has_request && !host.blank? && host !~ %r{^[-a-z]+://}
+ host = "#{@controller.request.protocol}#{host}"
end
+
+ "#{host}#{source}"
+ else
+ source
+ end
end
# Pick an asset host for this source. Returns +nil+ if no host is set,
@@ -591,7 +586,7 @@ module ActionView
expanded_sources = sources.collect do |source|
determine_source(source, @@javascript_expansions)
end.flatten
- expanded_sources << "application" if sources.include?(:defaults) && file_exist?(File.join(JAVASCRIPTS_DIR, "application.js"))
+ expanded_sources << "application" if sources.include?(:defaults) && File.exist?(File.join(JAVASCRIPTS_DIR, "application.js"))
expanded_sources
end
end
@@ -617,12 +612,21 @@ module ActionView
end
def join_asset_file_contents(paths)
- paths.collect { |path| File.read(File.join(ASSETS_DIR, path.split("?").first)) }.join("\n\n")
+ paths.collect { |path| File.read(asset_file_path(path)) }.join("\n\n")
end
def write_asset_file_contents(joined_asset_path, asset_paths)
FileUtils.mkdir_p(File.dirname(joined_asset_path))
File.open(joined_asset_path, "w+") { |cache| cache.write(join_asset_file_contents(asset_paths)) }
+
+ # Set mtime to the latest of the combined files to allow for
+ # consistent ETag without a shared filesystem.
+ mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max
+ File.utime(mt, mt, joined_asset_path)
+ end
+
+ def asset_file_path(path)
+ File.join(ASSETS_DIR, path.split('?').first)
end
def collect_asset_files(*path)
diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb
index ebb1cb34bc..e65d5d1f60 100644
--- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb
+++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb
@@ -17,7 +17,7 @@ module ActionView
# # GET /posts.atom
# def index
# @posts = Post.find(:all)
- #
+ #
# respond_to do |format|
# format.html
# format.atom
@@ -29,12 +29,12 @@ module ActionView
# atom_feed do |feed|
# feed.title("My great blog!")
# feed.updated((@posts.first.created_at))
- #
+ #
# for post in @posts
# feed.entry(post) do |entry|
# entry.title(post.title)
# entry.content(post.body, :type => 'html')
- #
+ #
# entry.author do |author|
# author.name("DHH")
# end
@@ -47,8 +47,9 @@ module ActionView
# * <tt>:language</tt>: Defaults to "en-US".
# * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host.
# * <tt>:url</tt>: The URL for this feed. Defaults to the current URL.
- # * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you
- # created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified,
+ # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.request_uri.split(".")[0]}"
+ # * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you
+ # created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified,
# 2005 is used (as an "I don't care" value).
#
# Other namespaces can be added to the root element:
@@ -81,7 +82,7 @@ module ActionView
else
options[:schema_date] = "2005" # The Atom spec copyright date
end
-
+
xml = options[:xml] || eval("xml", block.binding)
xml.instruct!
@@ -89,10 +90,10 @@ module ActionView
feed_opts.merge!(options).reject!{|k,v| !k.to_s.match(/^xml/)}
xml.feed(feed_opts) do
- xml.id("tag:#{request.host},#{options[:schema_date]}:#{request.request_uri.split(".")[0]}")
+ xml.id(options[:id] || "tag:#{request.host},#{options[:schema_date]}:#{request.request_uri.split(".")[0]}")
xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:root_url] || (request.protocol + request.host_with_port))
xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:url] || request.url)
-
+
yield AtomFeedBuilder.new(xml, self, options)
end
end
@@ -102,7 +103,7 @@ module ActionView
def initialize(xml, view, feed_options = {})
@xml, @view, @feed_options = xml, view, feed_options
end
-
+
# Accepts a Date or Time object and inserts it in the proper format. If nil is passed, current time in UTC is used.
def updated(date_or_time = nil)
@xml.updated((date_or_time || Time.now.utc).xmlschema)
@@ -115,9 +116,10 @@ module ActionView
# * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists.
# * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists.
# * <tt>:url</tt>: The URL for this entry. Defaults to the polymorphic_url for the record.
+ # * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}"
def entry(record, options = {})
- @xml.entry do
- @xml.id("tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}")
+ @xml.entry do
+ @xml.id(options[:id] || "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}")
if options[:published] || (record.respond_to?(:created_at) && record.created_at)
@xml.published((options[:published] || record.created_at).xmlschema)
diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb
index 2cdbae6e40..64d1ad2715 100644
--- a/actionpack/lib/action_view/helpers/cache_helper.rb
+++ b/actionpack/lib/action_view/helpers/cache_helper.rb
@@ -32,7 +32,7 @@ module ActionView
# <i>Topics listed alphabetically</i>
# <% end %>
def cache(name = {}, options = nil, &block)
- _last_render.handler.new(@controller).cache_fragment(block, name, options)
+ @controller.fragment_for(output_buffer, name, options, &block)
end
end
end
diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb
index 720e2da8cc..e86ca27f31 100644
--- a/actionpack/lib/action_view/helpers/capture_helper.rb
+++ b/actionpack/lib/action_view/helpers/capture_helper.rb
@@ -122,14 +122,15 @@ module ActionView
nil
end
- private
- def with_output_buffer(buf = '')
- self.output_buffer, old_buffer = buf, output_buffer
- yield
- output_buffer
- ensure
- self.output_buffer = old_buffer
- end
+ # Use an alternate output buffer for the duration of the block.
+ # Defaults to a new empty string.
+ def with_output_buffer(buf = '') #:nodoc:
+ self.output_buffer, old_buffer = buf, output_buffer
+ yield
+ output_buffer
+ ensure
+ self.output_buffer = old_buffer
+ end
end
end
end
diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb
index d018034ebe..953a2a9f86 100755..100644
--- a/actionpack/lib/action_view/helpers/date_helper.rb
+++ b/actionpack/lib/action_view/helpers/date_helper.rb
@@ -3,18 +3,16 @@ require 'action_view/helpers/tag_helper'
module ActionView
module Helpers
- # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods
- # share a number of common options that are as follows:
+ # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the
+ # select-type methods share a number of common options that are as follows:
#
- # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give
- # birthday[month] instead of date[month] if passed to the select_month method.
+ # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday"
+ # would give birthday[month] instead of date[month] if passed to the select_month method.
# * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date.
- # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true, the select_month
- # method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of "date[month]".
+ # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true,
+ # the select_month method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of
+ # "date[month]".
module DateHelper
- include ActionView::Helpers::TagHelper
- DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX')
-
# Reports the approximate distance in time between two Time or Date objects or integers as seconds.
# Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs
# Distances are reported based on the following table:
@@ -51,40 +49,45 @@ module ActionView
# distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute
# distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
# distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
- # distance_of_time_in_words(from_time, from_time + 4.years + 15.days + 30.minutes + 5.seconds) # => over 4 years
+ # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => over 4 years
#
# to_time = Time.now + 6.years + 19.days
# distance_of_time_in_words(from_time, to_time, true) # => over 6 years
# distance_of_time_in_words(to_time, from_time, true) # => over 6 years
# distance_of_time_in_words(Time.now, Time.now) # => less than a minute
#
- def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
+ def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {})
from_time = from_time.to_time if from_time.respond_to?(:to_time)
to_time = to_time.to_time if to_time.respond_to?(:to_time)
distance_in_minutes = (((to_time - from_time).abs)/60).round
distance_in_seconds = ((to_time - from_time).abs).round
- case distance_in_minutes
- when 0..1
- return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
- case distance_in_seconds
- when 0..4 then 'less than 5 seconds'
- when 5..9 then 'less than 10 seconds'
- when 10..19 then 'less than 20 seconds'
- when 20..39 then 'half a minute'
- when 40..59 then 'less than a minute'
- else '1 minute'
- end
-
- when 2..44 then "#{distance_in_minutes} minutes"
- when 45..89 then 'about 1 hour'
- when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
- when 1440..2879 then '1 day'
- when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
- when 43200..86399 then 'about 1 month'
- when 86400..525599 then "#{(distance_in_minutes / 43200).round} months"
- when 525600..1051199 then 'about 1 year'
- else "over #{(distance_in_minutes / 525600).round} years"
+ I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
+ case distance_in_minutes
+ when 0..1
+ return distance_in_minutes == 0 ?
+ locale.t(:less_than_x_minutes, :count => 1) :
+ locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds
+
+ case distance_in_seconds
+ when 0..4 then locale.t :less_than_x_seconds, :count => 5
+ when 5..9 then locale.t :less_than_x_seconds, :count => 10
+ when 10..19 then locale.t :less_than_x_seconds, :count => 20
+ when 20..39 then locale.t :half_a_minute
+ when 40..59 then locale.t :less_than_x_minutes, :count => 1
+ else locale.t :x_minutes, :count => 1
+ end
+
+ when 2..44 then locale.t :x_minutes, :count => distance_in_minutes
+ when 45..89 then locale.t :about_x_hours, :count => 1
+ when 90..1439 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round
+ when 1440..2879 then locale.t :x_days, :count => 1
+ when 2880..43199 then locale.t :x_days, :count => (distance_in_minutes / 1440).round
+ when 43200..86399 then locale.t :about_x_months, :count => 1
+ when 86400..525599 then locale.t :x_months, :count => (distance_in_minutes / 43200).round
+ when 525600..1051199 then locale.t :about_x_years, :count => 1
+ else locale.t :over_x_years, :count => (distance_in_minutes / 525600).round
+ end
end
end
@@ -102,17 +105,37 @@ module ActionView
alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
- # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by
- # +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash,
- # which accepts all the keys that each of the individual select builders do (like <tt>:use_month_numbers</tt> for select_month) as well as a range of
- # discard options. The discard options are <tt>:discard_year</tt>, <tt>:discard_month</tt> and <tt>:discard_day</tt>. Set to true, they'll
- # drop the respective select. Discarding the month select will also automatically discard the day select. It's also possible to explicitly
- # set the order of the tags using the <tt>:order</tt> option with an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in
- # the desired order. Symbols may be omitted and the respective select is not included.
- #
- # Pass the <tt>:default</tt> option to set the default date. Use a Time object or a Hash of <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt>, and <tt>:second</tt>.
- #
- # Passing <tt>:disabled => true</tt> as part of the +options+ will make elements inaccessible for change.
+ # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based
+ # attribute (identified by +method+) on an object assigned to the template (identified by +object+). You can
+ # the output in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g.
+ # "2" instead of "February").
+ # * <tt>:use_short_month</tt> - Set to true if you want to use the abbreviated month name instead of the full
+ # name (e.g. "Feb" instead of "February").
+ # * <tt>:add_month_number</tt> - Set to true if you want to show both, the month's number and name (e.g.
+ # "2 - February" instead of "February").
+ # * <tt>:use_month_names</tt> - Set to an array with 12 month names if you want to customize month names.
+ # Note: You can also use Rails' new i18n functionality for this.
+ # * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
+ # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Time.now.year - 5</tt>.
+ # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Time.now.year + 5</tt>.
+ # * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day
+ # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the
+ # first of the given month in order to not create invalid dates like 31 February.
+ # * <tt>:discard_month</tt> - Set to true if you don't want to show a month select. This includes the month
+ # as a hidden field instead of showing a select field. Also note that this implicitly sets :discard_day to true.
+ # * <tt>:discard_year</tt> - Set to true if you don't want to show a year select. This includes the year
+ # as a hidden field instead of showing a select field.
+ # * <tt>:order</tt> - Set to an array containing <tt>:day</tt>, <tt>:month</tt> and <tt>:year</tt> do
+ # customize the order in which the select fields are shown. If you leave out any of the symbols, the respective
+ # select will not be shown (like when you set <tt>:discard_xxx => true</tt>. Defaults to the order defined in
+ # the respective locale (e.g. [:year, :month, :day] in the en-US locale that ships with Rails).
+ # * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty
+ # dates.
+ # * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil.
+ # * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled.
#
# If anything is passed in the +html_options+ hash it will be applied to every select tag in the set.
#
@@ -128,7 +151,7 @@ module ActionView
#
# # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute,
# # with the year in the year drop down box starting at 1995, numbers used for months instead of words,
- # # and without a day select box.
+ # # and without a day select box.
# date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true,
# :discard_day => true, :include_blank => true)
#
@@ -150,15 +173,15 @@ module ActionView
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
#
- # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that all month
- # choices are valid.
+ # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
+ # all month choices are valid.
def date_select(object_name, method, options = {}, html_options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_date_select_tag(options, html_options)
end
- # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a specified
- # time-based attribute (identified by +method+) on an object assigned to the template (identified by +object+).
- # You can include the seconds with <tt>:include_seconds</tt>.
+ # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a
+ # specified time-based attribute (identified by +method+) on an object assigned to the template (identified by
+ # +object+). You can include the seconds with <tt>:include_seconds</tt>.
#
# This method will also generate 3 input hidden tags, for the actual year, month and day unless the option
# <tt>:ignore_date</tt> is set to +true+.
@@ -169,18 +192,19 @@ module ActionView
# # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute
# time_select("post", "sunrise")
#
- # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted attribute
+ # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted
+ # # attribute
# time_select("order", "submitted")
#
# # Creates a time select tag that, when POSTed, will be stored in the mail variable in the sent_at attribute
# time_select("mail", "sent_at")
#
- # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the post variables in
- # # the sunrise attribute.
+ # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the post variables in
+ # # the sunrise attribute.
# time_select("post", "start_time", :include_seconds => true)
#
- # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the entry variables in
- # # the submission_time attribute.
+ # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the entry variables in
+ # # the submission_time attribute.
# time_select("entry", "submission_time", :include_seconds => true)
#
# # You can set the :minute_step to 15 which will give you: 00, 15, 30 and 45.
@@ -188,31 +212,33 @@ module ActionView
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
#
- # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that all month
- # choices are valid.
+ # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
+ # all month choices are valid.
def time_select(object_name, method, options = {}, html_options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_time_select_tag(options, html_options)
end
- # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based
- # attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples:
+ # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a
+ # specified datetime-based attribute (identified by +method+) on an object assigned to the template (identified
+ # by +object+). Examples:
#
# If anything is passed in the html_options hash it will be applied to every select tag in the set.
#
# ==== Examples
- # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on attribute
+ # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on
+ # # attribute
# datetime_select("post", "written_on")
#
- # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the
+ # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the
# # post variable in the written_on attribute.
# datetime_select("post", "written_on", :start_year => 1995)
#
- # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will be stored in the
- # # trip variable in the departing attribute.
+ # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will
+ # # be stored in the trip variable in the departing attribute.
# datetime_select("trip", "departing", :default => 3.days.from_now)
#
- # # Generates a datetime select that discards the type that, when POSTed, will be stored in the post variable as the written_on
- # # attribute.
+ # # Generates a datetime select that discards the type that, when POSTed, will be stored in the post variable
+ # # as the written_on attribute.
# datetime_select("post", "written_on", :discard_type => true)
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
@@ -220,11 +246,12 @@ module ActionView
InstanceTag.new(object_name, method, self, options.delete(:object)).to_datetime_select_tag(options, html_options)
end
- # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+.
- # It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
- # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it
- # will be appended onto the <tt>:order</tt> passed in. You can also add <tt>:date_separator</tt> and <tt>:time_separator</tt>
- # keys to the +options+ to control visual display of the elements.
+ # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the
+ # +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with
+ # an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not
+ # supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add
+ # <tt>:date_separator</tt>, <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to
+ # control visual display of the elements.
#
# If anything is passed in the html_options hash it will be applied to every select tag in the set.
#
@@ -245,7 +272,12 @@ module ActionView
# # with a '/' between each date field.
# select_datetime(my_date_time, :date_separator => '/')
#
- # # Generates a datetime select that discards the type of the field and defaults to the datetime in
+ # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
+ # # with a date fields separated by '/', time fields separated by '' and the date and time fields
+ # # separated by a comma (',').
+ # select_datetime(my_date_time, :date_separator => '/', :time_separator => '', :datetime_separator => ',')
+ #
+ # # Generates a datetime select that discards the type of the field and defaults to the datetime in
# # my_date_time (four days after today)
# select_datetime(my_date_time, :discard_type => true)
#
@@ -254,14 +286,13 @@ module ActionView
# select_datetime(my_date_time, :prefix => 'payday')
#
def select_datetime(datetime = Time.current, options = {}, html_options = {})
- separator = options[:datetime_separator] || ''
- select_date(datetime, options, html_options) + separator + select_time(datetime, options, html_options)
- end
+ DateTimeSelector.new(datetime, options, html_options).select_datetime
+ end
# Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
# It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
- # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it
- # will be appended onto the <tt>:order</tt> passed in.
+ # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol,
+ # it will be appended onto the <tt>:order</tt> passed in.
#
# If anything is passed in the html_options hash it will be applied to every select tag in the set.
#
@@ -278,27 +309,24 @@ module ActionView
# # with the fields ordered year, month, day rather than month, day, year.
# select_date(my_date, :order => [:year, :month, :day])
#
- # # Generates a date select that discards the type of the field and defaults to the date in
+ # # Generates a date select that discards the type of the field and defaults to the date in
# # my_date (six days after today)
- # select_datetime(my_date_time, :discard_type => true)
+ # select_date(my_date, :discard_type => true)
+ #
+ # # Generates a date select that defaults to the date in my_date,
+ # # which has fields separated by '/'
+ # select_date(my_date, :date_separator => '/')
#
# # Generates a date select that defaults to the datetime in my_date (six days after today)
# # prefixed with 'payday' rather than 'date'
- # select_datetime(my_date_time, :prefix => 'payday')
+ # select_date(my_date, :prefix => 'payday')
#
def select_date(date = Date.current, options = {}, html_options = {})
- options[:order] ||= []
- [:year, :month, :day].each { |o| options[:order].push(o) unless options[:order].include?(o) }
-
- select_date = ''
- options[:order].each do |o|
- select_date << self.send("select_#{o}", date, options, html_options)
- end
- select_date
+ DateTimeSelector.new(date, options, html_options).select_date
end
# Returns a set of html select-tags (one for hour and minute)
- # You can set <tt>:time_separator</tt> key to format the output, and
+ # You can set <tt>:time_separator</tt> key to format the output, and
# the <tt>:include_seconds</tt> option to include an input for seconds.
#
# If anything is passed in the html_options hash it will be applied to every select tag in the set.
@@ -313,7 +341,7 @@ module ActionView
# select_time()
#
# # Generates a time select that defaults to the time in my_time,
- # # which has fields separated by ':'
+ # # which has fields separated by ':'
# select_time(my_time, :time_separator => ':')
#
# # Generates a time select that defaults to the time in my_time,
@@ -325,8 +353,7 @@ module ActionView
# select_time(my_time, :time_separator => ':', :include_seconds => true)
#
def select_time(datetime = Time.current, options = {}, html_options = {})
- separator = options[:time_separator] || ''
- select_hour(datetime, options, html_options) + separator + select_minute(datetime, options, html_options) + (options[:include_seconds] ? separator + select_second(datetime, options, html_options) : '')
+ DateTimeSelector.new(datetime, options, html_options).select_time
end
# Returns a select tag with options for each of the seconds 0 through 59 with the current second selected.
@@ -341,31 +368,18 @@ module ActionView
#
# # Generates a select field for seconds that defaults to the number given
# select_second(33)
- #
+ #
# # Generates a select field for seconds that defaults to the seconds for the time in my_time
# # that is named 'interval' rather than 'second'
# select_second(my_time, :field_name => 'interval')
#
def select_second(datetime, options = {}, html_options = {})
- val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) : ''
- if options[:use_hidden]
- options[:include_seconds] ? hidden_html(options[:field_name] || 'second', val, options) : ''
- else
- second_options = []
- 0.upto(59) do |second|
- second_options << ((val == second) ?
- content_tag(:option, leading_zero_on_single_digits(second), :value => leading_zero_on_single_digits(second), :selected => "selected") :
- content_tag(:option, leading_zero_on_single_digits(second), :value => leading_zero_on_single_digits(second))
- )
- second_options << "\n"
- end
- select_html(options[:field_name] || 'second', second_options.join, options, html_options)
- end
+ DateTimeSelector.new(datetime, options, html_options).select_second
end
# Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
- # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute selected
- # The <tt>minute</tt> can also be substituted for a minute number.
+ # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute
+ # selected. The <tt>minute</tt> can also be substituted for a minute number.
# Override the field name using the <tt>:field_name</tt> option, 'minute' by default.
#
# ==== Examples
@@ -376,26 +390,13 @@ module ActionView
#
# # Generates a select field for minutes that defaults to the number given
# select_minute(14)
- #
+ #
# # Generates a select field for minutes that defaults to the minutes for the time in my_time
# # that is named 'stride' rather than 'second'
# select_minute(my_time, :field_name => 'stride')
#
def select_minute(datetime, options = {}, html_options = {})
- val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.min) : ''
- if options[:use_hidden]
- hidden_html(options[:field_name] || 'minute', val, options)
- else
- minute_options = []
- 0.step(59, options[:minute_step] || 1) do |minute|
- minute_options << ((val == minute) ?
- content_tag(:option, leading_zero_on_single_digits(minute), :value => leading_zero_on_single_digits(minute), :selected => "selected") :
- content_tag(:option, leading_zero_on_single_digits(minute), :value => leading_zero_on_single_digits(minute))
- )
- minute_options << "\n"
- end
- select_html(options[:field_name] || 'minute', minute_options.join, options, html_options)
- end
+ DateTimeSelector.new(datetime, options, html_options).select_minute
end
# Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
@@ -410,26 +411,13 @@ module ActionView
#
# # Generates a select field for minutes that defaults to the number given
# select_minute(14)
- #
+ #
# # Generates a select field for minutes that defaults to the minutes for the time in my_time
# # that is named 'stride' rather than 'second'
# select_minute(my_time, :field_name => 'stride')
#
def select_hour(datetime, options = {}, html_options = {})
- val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) : ''
- if options[:use_hidden]
- hidden_html(options[:field_name] || 'hour', val, options)
- else
- hour_options = []
- 0.upto(23) do |hour|
- hour_options << ((val == hour) ?
- content_tag(:option, leading_zero_on_single_digits(hour), :value => leading_zero_on_single_digits(hour), :selected => "selected") :
- content_tag(:option, leading_zero_on_single_digits(hour), :value => leading_zero_on_single_digits(hour))
- )
- hour_options << "\n"
- end
- select_html(options[:field_name] || 'hour', hour_options.join, options, html_options)
- end
+ DateTimeSelector.new(datetime, options, html_options).select_hour
end
# Returns a select tag with options for each of the days 1 through 31 with the current day selected.
@@ -444,36 +432,23 @@ module ActionView
#
# # Generates a select field for days that defaults to the number given
# select_day(5)
- #
+ #
# # Generates a select field for days that defaults to the day for the date in my_date
# # that is named 'due' rather than 'day'
# select_day(my_time, :field_name => 'due')
#
def select_day(date, options = {}, html_options = {})
- val = date ? (date.kind_of?(Fixnum) ? date : date.day) : ''
- if options[:use_hidden]
- hidden_html(options[:field_name] || 'day', val, options)
- else
- day_options = []
- 1.upto(31) do |day|
- day_options << ((val == day) ?
- content_tag(:option, day, :value => day, :selected => "selected") :
- content_tag(:option, day, :value => day)
- )
- day_options << "\n"
- end
- select_html(options[:field_name] || 'day', day_options.join, options, html_options)
- end
+ DateTimeSelector.new(date, options, html_options).select_day
end
- # Returns a select tag with options for each of the months January through December with the current month selected.
- # The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values
- # (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names --
- # set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you want both numbers and names,
- # set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer to show month names as abbreviations,
- # set the <tt>:use_short_month</tt> key in +options+ to true. If you want to use your own month names, set the
- # <tt>:use_month_names</tt> key in +options+ to an array of 12 month names. Override the field name using the
- # <tt>:field_name</tt> option, 'month' by default.
+ # Returns a select tag with options for each of the months January through December with the current month
+ # selected. The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are
+ # used as values (what's submitted to the server). It's also possible to use month numbers for the presentation
+ # instead of names -- set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you
+ # want both numbers and names, set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer
+ # to show month names as abbreviations, set the <tt>:use_short_month</tt> key in +options+ to true. If you want
+ # to use your own month names, set the <tt>:use_month_names</tt> key in +options+ to an array of 12 month names.
+ # Override the field name using the <tt>:field_name</tt> option, 'month' by default.
#
# ==== Examples
# # Generates a select field for months that defaults to the current month that
@@ -485,7 +460,7 @@ module ActionView
# select_month(Date.today, :field_name => 'start')
#
# # Generates a select field for months that defaults to the current month that
- # # will use keys like "1", "3".
+ # # will use keys like "1", "3".
# select_month(Date.today, :use_month_numbers => true)
#
# # Generates a select field for months that defaults to the current month that
@@ -501,36 +476,14 @@ module ActionView
# select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...))
#
def select_month(date, options = {}, html_options = {})
- val = date ? (date.kind_of?(Fixnum) ? date : date.month) : ''
- if options[:use_hidden]
- hidden_html(options[:field_name] || 'month', val, options)
- else
- month_options = []
- month_names = options[:use_month_names] || (options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES)
- month_names.unshift(nil) if month_names.size < 13
- 1.upto(12) do |month_number|
- month_name = if options[:use_month_numbers]
- month_number
- elsif options[:add_month_numbers]
- month_number.to_s + ' - ' + month_names[month_number]
- else
- month_names[month_number]
- end
-
- month_options << ((val == month_number) ?
- content_tag(:option, month_name, :value => month_number, :selected => "selected") :
- content_tag(:option, month_name, :value => month_number)
- )
- month_options << "\n"
- end
- select_html(options[:field_name] || 'month', month_options.join, options, html_options)
- end
+ DateTimeSelector.new(date, options, html_options).select_month
end
- # Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius
- # can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the +options+. Both ascending and descending year
- # lists are supported by making <tt>:start_year</tt> less than or greater than <tt>:end_year</tt>. The <tt>date</tt> can also be
- # substituted for a year given as a number. Override the field name using the <tt>:field_name</tt> option, 'year' by default.
+ # Returns a select tag with options for each of the five years on each side of the current, which is selected.
+ # The five year radius can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the
+ # +options+. Both ascending and descending year lists are supported by making <tt>:start_year</tt> less than or
+ # greater than <tt>:end_year</tt>. The <tt>date</tt> can also be substituted for a year given as a number.
+ # Override the field name using the <tt>:field_name</tt> option, 'year' by default.
#
# ==== Examples
# # Generates a select field for years that defaults to the current year that
@@ -550,159 +503,384 @@ module ActionView
# select_year(2006, :start_year => 2000, :end_year => 2010)
#
def select_year(date, options = {}, html_options = {})
- if !date || date == 0
- value = ''
- middle_year = Date.today.year
- elsif date.kind_of?(Fixnum)
- value = middle_year = date
+ DateTimeSelector.new(date, options, html_options).select_year
+ end
+ end
+
+ class DateTimeSelector #:nodoc:
+ extend ActiveSupport::Memoizable
+ include ActionView::Helpers::TagHelper
+
+ DEFAULT_PREFIX = 'date'.freeze unless const_defined?('DEFAULT_PREFIX')
+ POSITION = {
+ :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6
+ }.freeze unless const_defined?('POSITION')
+
+ def initialize(datetime, options = {}, html_options = {})
+ @options = options.dup
+ @html_options = html_options.dup
+ @datetime = datetime
+ end
+
+ def select_datetime
+ # TODO: Remove tag conditional
+ # Ideally we could just join select_date and select_date for the tag case
+ if @options[:tag] && @options[:ignore_date]
+ select_time
+ elsif @options[:tag]
+ order = date_order.dup
+ order -= [:hour, :minute, :second]
+
+ @options[:discard_year] ||= true unless order.include?(:year)
+ @options[:discard_month] ||= true unless order.include?(:month)
+ @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
+ @options[:discard_minute] ||= true if @options[:discard_hour]
+ @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute]
+
+ # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
+ # valid (otherwise it could be 31 and february wouldn't be a valid date)
+ if @options[:discard_day] && !@options[:discard_month]
+ @datetime = @datetime.change(:day => 1)
+ end
+
+ [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
+ order += [:hour, :minute, :second] unless @options[:discard_hour]
+
+ build_selects_from_types(order)
else
- value = middle_year = date.year
+ "#{select_date}#{@options[:datetime_separator]}#{select_time}"
end
+ end
- if options[:use_hidden]
- hidden_html(options[:field_name] || 'year', value, options)
- else
- year_options = ''
- start_year = options[:start_year] || middle_year - 5
- end_year = options[:end_year] || middle_year + 5
- step_val = start_year < end_year ? 1 : -1
-
- start_year.step(end_year, step_val) do |year|
- if value == year
- year_options << content_tag(:option, year, :value => year, :selected => "selected")
- else
- year_options << content_tag(:option, year, :value => year)
- end
- year_options << "\n"
+ def select_date
+ order = date_order.dup
+
+ # TODO: Remove tag conditional
+ if @options[:tag]
+ @options[:discard_hour] = true
+ @options[:discard_minute] = true
+ @options[:discard_second] = true
+
+ @options[:discard_year] ||= true unless order.include?(:year)
+ @options[:discard_month] ||= true unless order.include?(:month)
+ @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
+
+ # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
+ # valid (otherwise it could be 31 and february wouldn't be a valid date)
+ if @options[:discard_day] && !@options[:discard_month]
+ @datetime = @datetime.change(:day => 1)
end
- select_html(options[:field_name] || 'year', year_options, options, html_options)
end
+
+ [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
+
+ build_selects_from_types(order)
end
- private
+ def select_time
+ order = []
- def select_html(type, html_options, options, select_tag_options = {})
- name_and_id_from_options(options, type)
- select_options = {:id => options[:id], :name => options[:name]}
- select_options.merge!(:disabled => 'disabled') if options[:disabled]
- select_options.merge!(select_tag_options) unless select_tag_options.empty?
- select_html = "\n"
- select_html << content_tag(:option, '', :value => '') + "\n" if options[:include_blank]
- select_html << html_options.to_s
- content_tag(:select, select_html, select_options) + "\n"
- end
+ # TODO: Remove tag conditional
+ if @options[:tag]
+ @options[:discard_month] = true
+ @options[:discard_year] = true
+ @options[:discard_day] = true
+ @options[:discard_second] ||= true unless @options[:include_seconds]
- def hidden_html(type, value, options)
- name_and_id_from_options(options, type)
- hidden_html = tag(:input, :type => "hidden", :id => options[:id], :name => options[:name], :value => value) + "\n"
+ order += [:year, :month, :day] unless @options[:ignore_date]
end
- def name_and_id_from_options(options, type)
- options[:name] = (options[:prefix] || DEFAULT_PREFIX) + (options[:discard_type] ? '' : "[#{type}]")
- options[:id] = options[:name].gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '')
+ order += [:hour, :minute]
+ order << :second if @options[:include_seconds]
+
+ build_selects_from_types(order)
+ end
+
+ def select_second
+ if @options[:use_hidden] || @options[:discard_second]
+ build_hidden(:second, sec) if @options[:include_seconds]
+ else
+ build_options_and_select(:second, sec)
end
+ end
- def leading_zero_on_single_digits(number)
- number > 9 ? number : "0#{number}"
+ def select_minute
+ if @options[:use_hidden] || @options[:discard_minute]
+ build_hidden(:minute, min)
+ else
+ build_options_and_select(:minute, min, :step => @options[:minute_step])
end
- end
+ end
- class InstanceTag #:nodoc:
- include DateHelper
+ def select_hour
+ if @options[:use_hidden] || @options[:discard_hour]
+ build_hidden(:hour, hour)
+ else
+ build_options_and_select(:hour, hour, :end => 23)
+ end
+ end
- def to_date_select_tag(options = {}, html_options = {})
- date_or_time_select(options.merge(:discard_hour => true), html_options)
+ def select_day
+ if @options[:use_hidden] || @options[:discard_day]
+ build_hidden(:day, day)
+ else
+ build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false)
+ end
end
- def to_time_select_tag(options = {}, html_options = {})
- date_or_time_select(options.merge(:discard_year => true, :discard_month => true), html_options)
+ def select_month
+ if @options[:use_hidden] || @options[:discard_month]
+ build_hidden(:month, month)
+ else
+ month_options = []
+ 1.upto(12) do |month_number|
+ options = { :value => month_number }
+ options[:selected] = "selected" if month == month_number
+ month_options << content_tag(:option, month_name(month_number), options) + "\n"
+ end
+ build_select(:month, month_options.join)
+ end
end
- def to_datetime_select_tag(options = {}, html_options = {})
- date_or_time_select(options, html_options)
+ def select_year
+ if !@datetime || @datetime == 0
+ val = ''
+ middle_year = Date.today.year
+ else
+ val = middle_year = year
+ end
+
+ if @options[:use_hidden] || @options[:discard_year]
+ build_hidden(:year, val)
+ else
+ options = {}
+ options[:start] = @options[:start_year] || middle_year - 5
+ options[:end] = @options[:end_year] || middle_year + 5
+ options[:step] = options[:start] < options[:end] ? 1 : -1
+ options[:leading_zeros] = false
+
+ build_options_and_select(:year, val, options)
+ end
end
private
- def date_or_time_select(options, html_options = {})
- defaults = { :discard_type => true }
- options = defaults.merge(options)
- datetime = value(object)
- datetime ||= default_time_from_options(options[:default]) unless options[:include_blank]
-
- position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
-
- order = (options[:order] ||= [:year, :month, :day])
-
- # Discard explicit and implicit by not being included in the :order
- discard = {}
- discard[:year] = true if options[:discard_year] or !order.include?(:year)
- discard[:month] = true if options[:discard_month] or !order.include?(:month)
- discard[:day] = true if options[:discard_day] or discard[:month] or !order.include?(:day)
- discard[:hour] = true if options[:discard_hour]
- discard[:minute] = true if options[:discard_minute] or discard[:hour]
- discard[:second] = true unless options[:include_seconds] && !discard[:minute]
-
- # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are valid
- # (otherwise it could be 31 and february wouldn't be a valid date)
- if datetime && discard[:day] && !discard[:month]
- datetime = datetime.change(:day => 1)
+ %w( sec min hour day month year ).each do |method|
+ define_method(method) do
+ @datetime.kind_of?(Fixnum) ? @datetime : @datetime.send(method) if @datetime
end
+ end
- # Maintain valid dates by including hidden fields for discarded elements
- [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
+ # Returns translated month names, but also ensures that a custom month
+ # name array has a leading nil element
+ def month_names
+ month_names = @options[:use_month_names] || translated_month_names
+ month_names.unshift(nil) if month_names.size < 13
+ month_names
+ end
+ memoize :month_names
+
+ # Returns translated month names
+ # => [nil, "January", "February", "March",
+ # "April", "May", "June", "July",
+ # "August", "September", "October",
+ # "November", "December"]
+ #
+ # If :use_short_month option is set
+ # => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ # "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+ def translated_month_names
+ begin
+ key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names'
+ I18n.translate(key, :locale => @options[:locale])
+ end
+ end
- # Ensure proper ordering of :hour, :minute and :second
- [:hour, :minute, :second].each { |o| order.delete(o); order.push(o) }
+ # Lookup month name for number
+ # month_name(1) => "January"
+ #
+ # If :use_month_numbers option is passed
+ # month_name(1) => 1
+ #
+ # If :add_month_numbers option is passed
+ # month_name(1) => "1 - January"
+ def month_name(number)
+ if @options[:use_month_numbers]
+ number
+ elsif @options[:add_month_numbers]
+ "#{number} - #{month_names[number]}"
+ else
+ month_names[number]
+ end
+ end
+
+ def date_order
+ @options[:order] || translated_date_order
+ end
+ memoize :date_order
- date_or_time_select = ''
- order.reverse.each do |param|
- # Send hidden fields for discarded elements once output has started
- # This ensures AR can reconstruct valid dates using ParseDate
- next if discard[param] && (date_or_time_select.empty? || options[:ignore_date])
+ def translated_date_order
+ begin
+ I18n.translate(:'date.order', :locale => @options[:locale]) || []
+ end
+ end
- date_or_time_select.insert(0, self.send("select_#{param}", datetime, options_with_prefix(position[param], options.merge(:use_hidden => discard[param])), html_options))
- date_or_time_select.insert(0,
- case param
- when :hour then (discard[:year] && discard[:day] ? "" : " &mdash; ")
- when :minute then " : "
- when :second then options[:include_seconds] ? " : " : ""
- else ""
- end)
+ # Build full select tag from date type and options
+ def build_options_and_select(type, selected, options = {})
+ build_select(type, build_options(selected, options))
+ end
+ # Build select option html from date value and options
+ # build_options(15, :start => 1, :end => 31)
+ # => "<option value="1">1</option>
+ # <option value=\"2\">2</option>
+ # <option value=\"3\">3</option>..."
+ def build_options(selected, options = {})
+ start = options.delete(:start) || 0
+ stop = options.delete(:end) || 59
+ step = options.delete(:step) || 1
+ leading_zeros = options.delete(:leading_zeros).nil? ? true : false
+
+ select_options = []
+ start.step(stop, step) do |i|
+ value = leading_zeros ? sprintf("%02d", i) : i
+ tag_options = { :value => value }
+ tag_options[:selected] = "selected" if selected == i
+ select_options << content_tag(:option, value, tag_options)
end
+ select_options.join("\n") + "\n"
+ end
- date_or_time_select
+ # Builds select tag from date type and html select options
+ # build_select(:month, "<option value="1">January</option>...")
+ # => "<select id="post_written_on_2i" name="post[written_on(2i)]">
+ # <option value="1">January</option>...
+ # </select>"
+ def build_select(type, select_options_as_html)
+ select_options = {
+ :id => input_id_from_type(type),
+ :name => input_name_from_type(type)
+ }.merge(@html_options)
+ select_options.merge!(:disabled => 'disabled') if @options[:disabled]
+
+ select_html = "\n"
+ select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
+ select_html << select_options_as_html.to_s
+
+ content_tag(:select, select_html, select_options) + "\n"
end
- def options_with_prefix(position, options)
- prefix = "#{@object_name}"
- if options[:index]
- prefix << "[#{options[:index]}]"
- elsif @auto_index
- prefix << "[#{@auto_index}]"
+ # Builds hidden input tag for date part and value
+ # build_hidden(:year, 2008)
+ # => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />"
+ def build_hidden(type, value)
+ tag(:input, {
+ :type => "hidden",
+ :id => input_id_from_type(type),
+ :name => input_name_from_type(type),
+ :value => value
+ }) + "\n"
+ end
+
+ # Returns the name attribute for the input tag
+ # => post[written_on(1i)]
+ def input_name_from_type(type)
+ prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX
+ prefix += "[#{@options[:index]}]" if @options[:index]
+
+ field_name = @options[:field_name] || type
+ if @options[:include_position]
+ field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)"
end
- options.merge(:prefix => "#{prefix}[#{@method_name}(#{position}i)]")
+
+ @options[:discard_type] ? prefix : "#{prefix}[#{field_name}]"
+ end
+
+ # Returns the id attribute for the input tag
+ # => "post_written_on_1i"
+ def input_id_from_type(type)
+ input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '')
+ end
+
+ # Given an ordering of datetime components, create the selection html
+ # and join them with their appropriate seperators
+ def build_selects_from_types(order)
+ select = ''
+ order.reverse.each do |type|
+ separator = separator(type) unless type == order.first # don't add on last field
+ select.insert(0, separator.to_s + send("select_#{type}").to_s)
+ end
+ select
+ end
+
+ # Returns the separator for a given datetime component
+ def separator(type)
+ case type
+ when :month, :day
+ @options[:date_separator]
+ when :hour
+ (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator]
+ when :minute
+ @options[:time_separator]
+ when :second
+ @options[:include_seconds] ? @options[:time_separator] : ""
+ end
+ end
+ end
+
+ class InstanceTag #:nodoc:
+ def to_date_select_tag(options = {}, html_options = {})
+ datetime_selector(options, html_options).select_date
+ end
+
+ def to_time_select_tag(options = {}, html_options = {})
+ datetime_selector(options, html_options).select_time
+ end
+
+ def to_datetime_select_tag(options = {}, html_options = {})
+ datetime_selector(options, html_options).select_datetime
+ end
+
+ private
+ def datetime_selector(options, html_options)
+ datetime = value(object) || default_datetime(options)
+
+ options = options.dup
+ options[:field_name] = @method_name
+ options[:include_position] = true
+ options[:prefix] ||= @object_name
+ options[:index] ||= @auto_index
+ options[:datetime_separator] ||= ' &mdash; '
+ options[:time_separator] ||= ' : '
+
+ DateTimeSelector.new(datetime, options.merge(:tag => true), html_options)
end
- def default_time_from_options(default)
- case default
+ def default_datetime(options)
+ return if options[:include_blank]
+
+ case options[:default]
when nil
Time.current
when Date, Time
- default
+ options[:default]
else
+ default = options[:default].dup
+
# Rename :minute and :second to :min and :sec
default[:min] ||= default[:minute]
default[:sec] ||= default[:second]
time = Time.current
-
+
[:year, :month, :day, :hour, :min, :sec].each do |key|
default[key] ||= time.send(key)
end
- Time.utc_time(default[:year], default[:month], default[:day], default[:hour], default[:min], default[:sec])
- end
+ Time.utc_time(
+ default[:year], default[:month], default[:day],
+ default[:hour], default[:min], default[:sec]
+ )
+ end
end
end
diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb
index 20de7e465f..90863fca08 100644
--- a/actionpack/lib/action_view/helpers/debug_helper.rb
+++ b/actionpack/lib/action_view/helpers/debug_helper.rb
@@ -2,21 +2,28 @@ module ActionView
module Helpers
# Provides a set of methods for making it easier to debug Rails objects.
module DebugHelper
- # Returns a <pre>-tag that has +object+ dumped by YAML. This creates a very
- # readable way to inspect an object.
+ # Returns a YAML representation of +object+ wrapped with <pre> and </pre>.
+ # If the object cannot be converted to YAML using +to_yaml+, +inspect+ will be called instead.
+ # Useful for inspecting an object at the time of rendering.
#
# ==== Example
- # my_hash = {'first' => 1, 'second' => 'two', 'third' => [1,2,3]}
- # debug(my_hash)
#
- # => <pre class='debug_dump'>---
- # first: 1
- # second: two
- # third:
- # - 1
- # - 2
- # - 3
- # </pre>
+ # @user = User.new({ :username => 'testing', :password => 'xyz', :age => 42}) %>
+ # debug(@user)
+ # # =>
+ # <pre class='debug_dump'>--- !ruby/object:User
+ # attributes:
+ # &nbsp; updated_at:
+ # &nbsp; username: testing
+ #
+ # &nbsp; age: 42
+ # &nbsp; password: xyz
+ # &nbsp; created_at:
+ # attributes_cache: {}
+ #
+ # new_record: true
+ # </pre>
+
def debug(object)
begin
Marshal::dump(object)
@@ -28,4 +35,4 @@ module ActionView
end
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_view/helpers/form_country_helper.rb b/actionpack/lib/action_view/helpers/form_country_helper.rb
new file mode 100644
index 0000000000..84e811f61d
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/form_country_helper.rb
@@ -0,0 +1,92 @@
+require 'action_view/helpers/form_options_helper'
+
+module ActionView
+ module Helpers
+ module FormCountryHelper
+
+ # Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags.
+ def country_select(object, method, priority_countries = nil, options = {}, html_options = {})
+ InstanceTag.new(object, method, self, options.delete(:object)).to_country_select_tag(priority_countries, options, html_options)
+ end
+
+ # Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to
+ # have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so
+ # that they will be listed above the rest of the (long) list.
+ #
+ # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
+ def country_options_for_select(selected = nil, priority_countries = nil)
+ country_options = ""
+
+ if priority_countries
+ country_options += options_for_select(priority_countries, selected)
+ country_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n"
+ end
+
+ return country_options + options_for_select(COUNTRIES, selected)
+ end
+
+ private
+
+ # All the countries included in the country_options output.
+ COUNTRIES = ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola",
+ "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria",
+ "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin",
+ "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil",
+ "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia",
+ "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China",
+ "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo",
+ "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba",
+ "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt",
+ "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)",
+ "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia",
+ "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea",
+ "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)",
+ "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq",
+ "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya",
+ "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan",
+ "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya",
+ "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of",
+ "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique",
+ "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of",
+ "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru",
+ "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger",
+ "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau",
+ "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines",
+ "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation",
+ "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia",
+ "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino",
+ "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore",
+ "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa",
+ "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname",
+ "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic",
+ "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste",
+ "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan",
+ "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom",
+ "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela",
+ "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara",
+ "Yemen", "Zambia", "Zimbabwe"] unless const_defined?("COUNTRIES")
+ end
+
+ class InstanceTag #:nodoc:
+ include FormCountryHelper
+
+ def to_country_select_tag(priority_countries, options, html_options)
+ html_options = html_options.stringify_keys
+ add_default_name_and_id(html_options)
+ value = value(object)
+ content_tag("select",
+ add_options(
+ country_options_for_select(value, priority_countries),
+ options, value
+ ), html_options
+ )
+ end
+ end
+
+ class FormBuilder
+ def country_select(method, priority_countries = nil, options = {}, html_options = {})
+ @template.country_select(@object_name, method, priority_countries, objectify_options(options), @default_options.merge(html_options))
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb
index bafc635ad2..7bb82ba5bb 100644
--- a/actionpack/lib/action_view/helpers/form_helper.rb
+++ b/actionpack/lib/action_view/helpers/form_helper.rb
@@ -76,7 +76,7 @@ module ActionView
# Creates a form and a scope around a specific model object that is used as
# a base for questioning about values for the fields.
#
- # Rails provides succint resource-oriented form generation with +form_for+
+ # Rails provides succinct resource-oriented form generation with +form_for+
# like this:
#
# <% form_for @offer do |f| %>
@@ -304,10 +304,6 @@ module ActionView
when String, Symbol
object_name = record_or_name_or_array
object = args.first
- when Array
- object = record_or_name_or_array.last
- object_name = ActionController::RecordIdentifier.singular_class_name(object)
- apply_form_for_options!(record_or_name_or_array, options)
else
object = record_or_name_or_array
object_name = ActionController::RecordIdentifier.singular_class_name(object)
@@ -449,8 +445,37 @@ module ActionView
# assigned to the template (identified by +object+). It's intended that +method+ returns an integer and if that
# integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a
# hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+
- # is set to 0 which is convenient for boolean values. Since HTTP standards say that unchecked checkboxes don't post anything,
- # we add a hidden value with the same name as the checkbox as a work around.
+ # is set to 0 which is convenient for boolean values.
+ #
+ # ==== Gotcha
+ #
+ # The HTML specification says unchecked check boxes are not successful, and
+ # thus web browsers do not send them. Unfortunately this introduces a gotcha:
+ # if an Invoice model has a +paid+ flag, and in the form that edits a paid
+ # invoice the user unchecks its check box, no +paid+ parameter is sent. So,
+ # any mass-assignment idiom like
+ #
+ # @invoice.update_attributes(params[:invoice])
+ #
+ # wouldn't update the flag.
+ #
+ # To prevent this the helper generates a hidden field with the same name as
+ # the checkbox after the very check box. So, the client either sends only the
+ # hidden field (representing the check box is unchecked), or both fields.
+ # Since the HTML specification says key/value pairs have to be sent in the
+ # same order they appear in the form and Rails parameters extraction always
+ # gets the first occurrence of any given key, that works in ordinary forms.
+ #
+ # Unfortunately that workaround does not work when the check box goes
+ # within an array-like parameter, as in
+ #
+ # <% fields_for "project[invoice_attributes][]", invoice, :index => nil do |form| %>
+ # <%= form.check_box :paid %>
+ # ...
+ # <% end %>
+ #
+ # because parameter name repetition is precisely what Rails seeks to distinguish
+ # the elements of the array.
#
# ==== Examples
# # Let's say that @post.validated? is 1:
@@ -503,10 +528,10 @@ module ActionView
def initialize(object_name, method_name, template_object, object = nil)
@object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
- @template_object= template_object
+ @template_object = template_object
@object = object
- if @object_name.sub!(/\[\]$/,"")
- if object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
+ if @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
+ if (object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
@auto_index = object.to_param
else
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
@@ -683,7 +708,7 @@ module ActionView
end
def sanitized_object_name
- @sanitized_object_name ||= @object_name.gsub(/[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
+ @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
end
def sanitized_method_name
@@ -701,6 +726,13 @@ module ActionView
def initialize(object_name, object, template, options, proc)
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
@default_options = @options ? @options.slice(:index) : {}
+ if @object_name.to_s.match(/\[\]$/)
+ if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
+ @auto_index = object.to_param
+ else
+ raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
+ end
+ end
end
(field_helpers - %w(label check_box radio_button fields_for)).each do |selector|
@@ -713,16 +745,25 @@ module ActionView
end
def fields_for(record_or_name_or_array, *args, &block)
+ if options.has_key?(:index)
+ index = "[#{options[:index]}]"
+ elsif defined?(@auto_index)
+ self.object_name = @object_name.to_s.sub(/\[\]$/,"")
+ index = "[#{@auto_index}]"
+ else
+ index = ""
+ end
+
case record_or_name_or_array
when String, Symbol
- name = "#{object_name}[#{record_or_name_or_array}]"
+ name = "#{object_name}#{index}[#{record_or_name_or_array}]"
when Array
object = record_or_name_or_array.last
- name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
+ name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
args.unshift(object)
else
object = record_or_name_or_array
- name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
+ name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
args.unshift(object)
end
@@ -741,8 +782,8 @@ module ActionView
@template.radio_button(@object_name, method, tag_value, objectify_options(options))
end
- def error_message_on(method, prepend_text = "", append_text = "", css_class = "formError")
- @template.error_message_on(@object, method, prepend_text, append_text, css_class)
+ def error_message_on(method, *args)
+ @template.error_message_on(@object, method, *args)
end
def error_messages(options = {})
diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb
index 87d49397c6..9aae945408 100644
--- a/actionpack/lib/action_view/helpers/form_options_helper.rb
+++ b/actionpack/lib/action_view/helpers/form_options_helper.rb
@@ -133,11 +133,6 @@ module ActionView
InstanceTag.new(object, method, self, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options)
end
- # Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags.
- def country_select(object, method, priority_countries = nil, options = {}, html_options = {})
- InstanceTag.new(object, method, self, options.delete(:object)).to_country_select_tag(priority_countries, options, html_options)
- end
-
# Return select and option tags for the given object and method, using
# #time_zone_options_for_select to generate the list of option tags.
#
@@ -274,22 +269,6 @@ module ActionView
end
end
- # Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to
- # have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so
- # that they will be listed above the rest of the (long) list.
- #
- # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
- def country_options_for_select(selected = nil, priority_countries = nil)
- country_options = ""
-
- if priority_countries
- country_options += options_for_select(priority_countries, selected)
- country_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n"
- end
-
- return country_options + options_for_select(COUNTRIES, selected)
- end
-
# Returns a string of option tags for pretty much any time zone in the
# world. Supply a TimeZone name as +selected+ to have it marked as the
# selected option tag. You can also supply an array of TimeZone objects
@@ -347,43 +326,7 @@ module ActionView
end
# All the countries included in the country_options output.
- COUNTRIES = ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola",
- "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria",
- "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin",
- "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil",
- "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia",
- "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China",
- "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo",
- "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba",
- "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt",
- "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)",
- "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia",
- "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea",
- "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)",
- "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq",
- "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya",
- "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan",
- "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya",
- "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of",
- "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique",
- "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of",
- "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru",
- "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger",
- "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau",
- "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines",
- "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation",
- "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia",
- "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino",
- "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore",
- "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa",
- "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname",
- "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic",
- "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste",
- "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan",
- "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom",
- "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela",
- "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara",
- "Yemen", "Zambia", "Zimbabwe"] unless const_defined?("COUNTRIES")
+ COUNTRIES = ActiveSupport::Deprecation::DeprecatedConstantProxy.new 'COUNTRIES', 'ActionView::Helpers::FormCountryHelper::COUNTRIES'
end
class InstanceTag #:nodoc:
@@ -406,18 +349,6 @@ module ActionView
)
end
- def to_country_select_tag(priority_countries, options, html_options)
- html_options = html_options.stringify_keys
- add_default_name_and_id(html_options)
- value = value(object)
- content_tag("select",
- add_options(
- country_options_for_select(value, priority_countries),
- options, value
- ), html_options
- )
- end
-
def to_time_zone_select_tag(priority_zones, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
@@ -452,10 +383,6 @@ module ActionView
@template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
end
- def country_select(method, priority_countries = nil, options = {}, html_options = {})
- @template.country_select(@object_name, method, priority_countries, objectify_options(options), @default_options.merge(html_options))
- end
-
def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
@template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
end
diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb
index bdfb2eebd7..e8ca02d760 100644
--- a/actionpack/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb
@@ -129,7 +129,7 @@ module ActionView
# label_tag 'name', nil, :class => 'small_label'
# # => <label for="name" class="small_label">Name</label>
def label_tag(name, text = nil, options = {})
- content_tag :label, text || name.humanize, { "for" => name }.update(options.stringify_keys)
+ content_tag :label, text || name.to_s.humanize, { "for" => name }.update(options.stringify_keys)
end
# Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or
diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb
index 22bd5cb440..32089442b7 100644
--- a/actionpack/lib/action_view/helpers/javascript_helper.rb
+++ b/actionpack/lib/action_view/helpers/javascript_helper.rb
@@ -44,13 +44,22 @@ module ActionView
include PrototypeHelper
- # Returns a link that will trigger a JavaScript +function+ using the
+ # Returns a link of the given +name+ that will trigger a JavaScript +function+ using the
# onclick handler and return false after the fact.
#
+ # The first argument +name+ is used as the link text.
+ #
+ # The next arguments are optional and may include the javascript function definition and a hash of html_options.
+ #
# The +function+ argument can be omitted in favor of an +update_page+
# block, which evaluates to a string when the template is rendered
# (instead of making an Ajax request first).
#
+ # The +html_options+ will accept a hash of html attributes for the link tag. Some examples are :class => "nav_button", :id => "articles_nav_button"
+ #
+ # Note: if you choose to specify the javascript function in a block, but would like to pass html_options, set the +function+ parameter to nil
+ #
+ #
# Examples:
# link_to_function "Greeting", "alert('Hello world!')"
# Produces:
@@ -89,13 +98,21 @@ module ActionView
content_tag(:a, name, html_options.merge(:href => href, :onclick => onclick))
end
- # Returns a button that'll trigger a JavaScript +function+ using the
+ # Returns a button with the given +name+ text that'll trigger a JavaScript +function+ using the
# onclick handler.
#
+ # The first argument +name+ is used as the button's value or display text.
+ #
+ # The next arguments are optional and may include the javascript function definition and a hash of html_options.
+ #
# The +function+ argument can be omitted in favor of an +update_page+
# block, which evaluates to a string when the template is rendered
# (instead of making an Ajax request first).
#
+ # The +html_options+ will accept a hash of html attributes for the link tag. Some examples are :class => "nav_button", :id => "articles_nav_button"
+ #
+ # Note: if you choose to specify the javascript function in a block, but would like to pass html_options, set the +function+ parameter to nil
+ #
# Examples:
# button_to_function "Greeting", "alert('Hello world!')"
# button_to_function "Delete", "if (confirm('Really?')) do_delete()"
diff --git a/actionpack/lib/action_view/helpers/javascripts/prototype.js b/actionpack/lib/action_view/helpers/javascripts/prototype.js
index 546f9fe449..2c70b8a7e8 100644
--- a/actionpack/lib/action_view/helpers/javascripts/prototype.js
+++ b/actionpack/lib/action_view/helpers/javascripts/prototype.js
@@ -1,5 +1,5 @@
-/* Prototype JavaScript framework, version 1.6.0.1
- * (c) 2005-2007 Sam Stephenson
+/* Prototype JavaScript framework, version 1.6.0.2
+ * (c) 2005-2008 Sam Stephenson
*
* Prototype is freely distributable under the terms of an MIT-style license.
* For details, see the Prototype web site: http://www.prototypejs.org/
@@ -7,7 +7,7 @@
*--------------------------------------------------------------------------*/
var Prototype = {
- Version: '1.6.0.1',
+ Version: '1.6.0.2',
Browser: {
IE: !!(window.attachEvent && !window.opera),
@@ -110,7 +110,7 @@ Object.extend(Object, {
try {
if (Object.isUndefined(object)) return 'undefined';
if (object === null) return 'null';
- return object.inspect ? object.inspect() : object.toString();
+ return object.inspect ? object.inspect() : String(object);
} catch (e) {
if (e instanceof RangeError) return '...';
throw e;
@@ -171,7 +171,8 @@ Object.extend(Object, {
},
isArray: function(object) {
- return object && object.constructor === Array;
+ return object != null && typeof object == "object" &&
+ 'splice' in object && 'join' in object;
},
isHash: function(object) {
@@ -578,7 +579,7 @@ var Template = Class.create({
}
return before + String.interpret(ctx);
- }.bind(this));
+ });
}
});
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
@@ -806,20 +807,20 @@ Object.extend(Enumerable, {
function $A(iterable) {
if (!iterable) return [];
if (iterable.toArray) return iterable.toArray();
- var length = iterable.length, results = new Array(length);
+ var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length];
return results;
}
if (Prototype.Browser.WebKit) {
- function $A(iterable) {
+ $A = function(iterable) {
if (!iterable) return [];
if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
iterable.toArray) return iterable.toArray();
- var length = iterable.length, results = new Array(length);
+ var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length];
return results;
- }
+ };
}
Array.from = $A;
@@ -1298,7 +1299,7 @@ Ajax.Request = Class.create(Ajax.Base, {
var contentType = response.getHeader('Content-type');
if (this.options.evalJS == 'force'
- || (this.options.evalJS && contentType
+ || (this.options.evalJS && this.isSameOrigin() && contentType
&& contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
this.evalResponse();
}
@@ -1316,9 +1317,18 @@ Ajax.Request = Class.create(Ajax.Base, {
}
},
+ isSameOrigin: function() {
+ var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
+ return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
+ protocol: location.protocol,
+ domain: document.domain,
+ port: location.port ? ':' + location.port : ''
+ }));
+ },
+
getHeader: function(name) {
try {
- return this.transport.getResponseHeader(name);
+ return this.transport.getResponseHeader(name) || null;
} catch (e) { return null }
},
@@ -1391,7 +1401,8 @@ Ajax.Response = Class.create({
if (!json) return null;
json = decodeURIComponent(escape(json));
try {
- return json.evalJSON(this.request.options.sanitizeJSON);
+ return json.evalJSON(this.request.options.sanitizeJSON ||
+ !this.request.isSameOrigin());
} catch (e) {
this.request.dispatchException(e);
}
@@ -1404,7 +1415,8 @@ Ajax.Response = Class.create({
this.responseText.blank())
return null;
try {
- return this.responseText.evalJSON(options.sanitizeJSON);
+ return this.responseText.evalJSON(options.sanitizeJSON ||
+ !this.request.isSameOrigin());
} catch (e) {
this.request.dispatchException(e);
}
@@ -1608,24 +1620,28 @@ Element.Methods = {
Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
insertions = {bottom:insertions};
- var content, t, range;
+ var content, insert, tagName, childNodes;
- for (position in insertions) {
+ for (var position in insertions) {
content = insertions[position];
position = position.toLowerCase();
- t = Element._insertionTranslations[position];
+ insert = Element._insertionTranslations[position];
if (content && content.toElement) content = content.toElement();
if (Object.isElement(content)) {
- t.insert(element, content);
+ insert(element, content);
continue;
}
content = Object.toHTML(content);
- range = element.ownerDocument.createRange();
- t.initializeRange(element, range);
- t.insert(element, range.createContextualFragment(content.stripScripts()));
+ tagName = ((position == 'before' || position == 'after')
+ ? element.parentNode : element).tagName.toUpperCase();
+
+ childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+
+ if (position == 'top' || position == 'after') childNodes.reverse();
+ childNodes.each(insert.curry(element));
content.evalScripts.bind(content).defer();
}
@@ -1670,7 +1686,7 @@ Element.Methods = {
},
descendants: function(element) {
- return $(element).getElementsBySelector("*");
+ return $(element).select("*");
},
firstDescendant: function(element) {
@@ -1709,32 +1725,31 @@ Element.Methods = {
element = $(element);
if (arguments.length == 1) return $(element.parentNode);
var ancestors = element.ancestors();
- return expression ? Selector.findElement(ancestors, expression, index) :
- ancestors[index || 0];
+ return Object.isNumber(expression) ? ancestors[expression] :
+ Selector.findElement(ancestors, expression, index);
},
down: function(element, expression, index) {
element = $(element);
if (arguments.length == 1) return element.firstDescendant();
- var descendants = element.descendants();
- return expression ? Selector.findElement(descendants, expression, index) :
- descendants[index || 0];
+ return Object.isNumber(expression) ? element.descendants()[expression] :
+ element.select(expression)[index || 0];
},
previous: function(element, expression, index) {
element = $(element);
if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
var previousSiblings = element.previousSiblings();
- return expression ? Selector.findElement(previousSiblings, expression, index) :
- previousSiblings[index || 0];
+ return Object.isNumber(expression) ? previousSiblings[expression] :
+ Selector.findElement(previousSiblings, expression, index);
},
next: function(element, expression, index) {
element = $(element);
if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
var nextSiblings = element.nextSiblings();
- return expression ? Selector.findElement(nextSiblings, expression, index) :
- nextSiblings[index || 0];
+ return Object.isNumber(expression) ? nextSiblings[expression] :
+ Selector.findElement(nextSiblings, expression, index);
},
select: function() {
@@ -1860,7 +1875,8 @@ Element.Methods = {
do { ancestor = ancestor.parentNode; }
while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
}
- if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex);
+ if (nextAncestor && nextAncestor.sourceIndex)
+ return (e > a && e < nextAncestor.sourceIndex);
}
while (element = element.parentNode)
@@ -2004,7 +2020,7 @@ Element.Methods = {
if (element) {
if (element.tagName == 'BODY') break;
var p = Element.getStyle(element, 'position');
- if (p == 'relative' || p == 'absolute') break;
+ if (p !== 'static') break;
}
} while (element);
return Element._returnOffset(valueL, valueT);
@@ -2153,46 +2169,6 @@ Element._attributeTranslations = {
}
};
-
-if (!document.createRange || Prototype.Browser.Opera) {
- Element.Methods.insert = function(element, insertions) {
- element = $(element);
-
- if (Object.isString(insertions) || Object.isNumber(insertions) ||
- Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
- insertions = { bottom: insertions };
-
- var t = Element._insertionTranslations, content, position, pos, tagName;
-
- for (position in insertions) {
- content = insertions[position];
- position = position.toLowerCase();
- pos = t[position];
-
- if (content && content.toElement) content = content.toElement();
- if (Object.isElement(content)) {
- pos.insert(element, content);
- continue;
- }
-
- content = Object.toHTML(content);
- tagName = ((position == 'before' || position == 'after')
- ? element.parentNode : element).tagName.toUpperCase();
-
- if (t.tags[tagName]) {
- var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
- if (position == 'top' || position == 'after') fragments.reverse();
- fragments.each(pos.insert.curry(element));
- }
- else element.insertAdjacentHTML(pos.adjacency, content.stripScripts());
-
- content.evalScripts.bind(content).defer();
- }
-
- return element;
- };
-}
-
if (Prototype.Browser.Opera) {
Element.Methods.getStyle = Element.Methods.getStyle.wrap(
function(proceed, element, style) {
@@ -2237,12 +2213,31 @@ if (Prototype.Browser.Opera) {
}
else if (Prototype.Browser.IE) {
- $w('positionedOffset getOffsetParent viewportOffset').each(function(method) {
+ // IE doesn't report offsets correctly for static elements, so we change them
+ // to "relative" to get the values, then change them back.
+ Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
+ function(proceed, element) {
+ element = $(element);
+ var position = element.getStyle('position');
+ if (position !== 'static') return proceed(element);
+ element.setStyle({ position: 'relative' });
+ var value = proceed(element);
+ element.setStyle({ position: position });
+ return value;
+ }
+ );
+
+ $w('positionedOffset viewportOffset').each(function(method) {
Element.Methods[method] = Element.Methods[method].wrap(
function(proceed, element) {
element = $(element);
var position = element.getStyle('position');
- if (position != 'static') return proceed(element);
+ if (position !== 'static') return proceed(element);
+ // Trigger hasLayout on the offset parent so that IE6 reports
+ // accurate offsetTop and offsetLeft values for position: fixed.
+ var offsetParent = element.getOffsetParent();
+ if (offsetParent && offsetParent.getStyle('position') === 'fixed')
+ offsetParent.setStyle({ zoom: 1 });
element.setStyle({ position: 'relative' });
var value = proceed(element);
element.setStyle({ position: position });
@@ -2324,7 +2319,10 @@ else if (Prototype.Browser.IE) {
};
Element._attributeTranslations.write = {
- names: Object.clone(Element._attributeTranslations.read.names),
+ names: Object.extend({
+ cellpadding: 'cellPadding',
+ cellspacing: 'cellSpacing'
+ }, Element._attributeTranslations.read.names),
values: {
checked: function(element, value) {
element.checked = !!value;
@@ -2444,7 +2442,7 @@ if (Prototype.Browser.IE || Prototype.Browser.Opera) {
};
}
-if (document.createElement('div').outerHTML) {
+if ('outerHTML' in document.createElement('div')) {
Element.Methods.replace = function(element, content) {
element = $(element);
@@ -2482,45 +2480,25 @@ Element._returnOffset = function(l, t) {
Element._getContentFromAnonymousElement = function(tagName, html) {
var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
- div.innerHTML = t[0] + html + t[1];
- t[2].times(function() { div = div.firstChild });
+ if (t) {
+ div.innerHTML = t[0] + html + t[1];
+ t[2].times(function() { div = div.firstChild });
+ } else div.innerHTML = html;
return $A(div.childNodes);
};
Element._insertionTranslations = {
- before: {
- adjacency: 'beforeBegin',
- insert: function(element, node) {
- element.parentNode.insertBefore(node, element);
- },
- initializeRange: function(element, range) {
- range.setStartBefore(element);
- }
+ before: function(element, node) {
+ element.parentNode.insertBefore(node, element);
},
- top: {
- adjacency: 'afterBegin',
- insert: function(element, node) {
- element.insertBefore(node, element.firstChild);
- },
- initializeRange: function(element, range) {
- range.selectNodeContents(element);
- range.collapse(true);
- }
+ top: function(element, node) {
+ element.insertBefore(node, element.firstChild);
},
- bottom: {
- adjacency: 'beforeEnd',
- insert: function(element, node) {
- element.appendChild(node);
- }
+ bottom: function(element, node) {
+ element.appendChild(node);
},
- after: {
- adjacency: 'afterEnd',
- insert: function(element, node) {
- element.parentNode.insertBefore(node, element.nextSibling);
- },
- initializeRange: function(element, range) {
- range.setStartAfter(element);
- }
+ after: function(element, node) {
+ element.parentNode.insertBefore(node, element.nextSibling);
},
tags: {
TABLE: ['<table>', '</table>', 1],
@@ -2532,7 +2510,6 @@ Element._insertionTranslations = {
};
(function() {
- this.bottom.initializeRange = this.top.initializeRange;
Object.extend(this.tags, {
THEAD: this.tags.TBODY,
TFOOT: this.tags.TBODY,
@@ -2716,7 +2693,7 @@ document.viewport = {
window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
}
};
-/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
* part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
* license. Please see http://www.yui-ext.com/ for more information. */
@@ -2959,13 +2936,13 @@ Object.extend(Selector, {
},
criteria: {
- tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
- className: 'n = h.className(n, r, "#{1}", c); c = false;',
- id: 'n = h.id(n, r, "#{1}", c); c = false;',
- attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
+ tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
+ className: 'n = h.className(n, r, "#{1}", c); c = false;',
+ id: 'n = h.id(n, r, "#{1}", c); c = false;',
+ attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
attr: function(m) {
m[3] = (m[5] || m[6]);
- return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
+ return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
},
pseudo: function(m) {
if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
@@ -2989,7 +2966,8 @@ Object.extend(Selector, {
tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
id: /^#([\w\-\*]+)(\b|$)/,
className: /^\.([\w\-\*]+)(\b|$)/,
- pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/,
+ pseudo:
+/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
attrPresence: /^\[([\w]+)\]/,
attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
},
@@ -3014,7 +2992,7 @@ Object.extend(Selector, {
attr: function(element, matches) {
var nodeValue = Element.readAttribute(element, matches[1]);
- return Selector.operators[matches[2]](nodeValue, matches[3]);
+ return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
}
},
@@ -3029,14 +3007,15 @@ Object.extend(Selector, {
// marks an array of nodes for counting
mark: function(nodes) {
+ var _true = Prototype.emptyFunction;
for (var i = 0, node; node = nodes[i]; i++)
- node._counted = true;
+ node._countedByPrototype = _true;
return nodes;
},
unmark: function(nodes) {
for (var i = 0, node; node = nodes[i]; i++)
- node._counted = undefined;
+ node._countedByPrototype = undefined;
return nodes;
},
@@ -3044,15 +3023,15 @@ Object.extend(Selector, {
// "ofType" flag indicates whether we're indexing for nth-of-type
// rather than nth-child
index: function(parentNode, reverse, ofType) {
- parentNode._counted = true;
+ parentNode._countedByPrototype = Prototype.emptyFunction;
if (reverse) {
for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
var node = nodes[i];
- if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
}
} else {
for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
- if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
}
},
@@ -3061,8 +3040,8 @@ Object.extend(Selector, {
if (nodes.length == 0) return nodes;
var results = [], n;
for (var i = 0, l = nodes.length; i < l; i++)
- if (!(n = nodes[i])._counted) {
- n._counted = true;
+ if (!(n = nodes[i])._countedByPrototype) {
+ n._countedByPrototype = Prototype.emptyFunction;
results.push(Element.extend(n));
}
return Selector.handlers.unmark(results);
@@ -3114,7 +3093,7 @@ Object.extend(Selector, {
// TOKEN FUNCTIONS
tagName: function(nodes, root, tagName, combinator) {
- tagName = tagName.toUpperCase();
+ var uTagName = tagName.toUpperCase();
var results = [], h = Selector.handlers;
if (nodes) {
if (combinator) {
@@ -3127,7 +3106,7 @@ Object.extend(Selector, {
if (tagName == "*") return nodes;
}
for (var i = 0, node; node = nodes[i]; i++)
- if (node.tagName.toUpperCase() == tagName) results.push(node);
+ if (node.tagName.toUpperCase() === uTagName) results.push(node);
return results;
} else return root.getElementsByTagName(tagName);
},
@@ -3174,16 +3153,18 @@ Object.extend(Selector, {
return results;
},
- attrPresence: function(nodes, root, attr) {
+ attrPresence: function(nodes, root, attr, combinator) {
if (!nodes) nodes = root.getElementsByTagName("*");
+ if (nodes && combinator) nodes = this[combinator](nodes);
var results = [];
for (var i = 0, node; node = nodes[i]; i++)
if (Element.hasAttribute(node, attr)) results.push(node);
return results;
},
- attr: function(nodes, root, attr, value, operator) {
+ attr: function(nodes, root, attr, value, operator, combinator) {
if (!nodes) nodes = root.getElementsByTagName("*");
+ if (nodes && combinator) nodes = this[combinator](nodes);
var handler = Selector.operators[operator], results = [];
for (var i = 0, node; node = nodes[i]; i++) {
var nodeValue = Element.readAttribute(node, attr);
@@ -3262,7 +3243,7 @@ Object.extend(Selector, {
var h = Selector.handlers, results = [], indexed = [], m;
h.mark(nodes);
for (var i = 0, node; node = nodes[i]; i++) {
- if (!node.parentNode._counted) {
+ if (!node.parentNode._countedByPrototype) {
h.index(node.parentNode, reverse, ofType);
indexed.push(node.parentNode);
}
@@ -3300,7 +3281,7 @@ Object.extend(Selector, {
var exclusions = new Selector(selector).findElements(root);
h.mark(exclusions);
for (var i = 0, results = [], node; node = nodes[i]; i++)
- if (!node._counted) results.push(node);
+ if (!node._countedByPrototype) results.push(node);
h.unmark(exclusions);
return results;
},
@@ -3334,11 +3315,19 @@ Object.extend(Selector, {
'|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
},
+ split: function(expression) {
+ var expressions = [];
+ expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+ expressions.push(m[1].strip());
+ });
+ return expressions;
+ },
+
matchElements: function(elements, expression) {
- var matches = new Selector(expression).findElements(), h = Selector.handlers;
+ var matches = $$(expression), h = Selector.handlers;
h.mark(matches);
for (var i = 0, results = [], element; element = elements[i]; i++)
- if (element._counted) results.push(element);
+ if (element._countedByPrototype) results.push(element);
h.unmark(matches);
return results;
},
@@ -3351,11 +3340,7 @@ Object.extend(Selector, {
},
findChildElements: function(element, expressions) {
- var exprs = expressions.join(',');
- expressions = [];
- exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
- expressions.push(m[1].strip());
- });
+ expressions = Selector.split(expressions.join(','));
var results = [], h = Selector.handlers;
for (var i = 0, l = expressions.length, selector; i < l; i++) {
selector = new Selector(expressions[i].strip());
@@ -3366,13 +3351,22 @@ Object.extend(Selector, {
});
if (Prototype.Browser.IE) {
- // IE returns comment nodes on getElementsByTagName("*").
- // Filter them out.
- Selector.handlers.concat = function(a, b) {
- for (var i = 0, node; node = b[i]; i++)
- if (node.tagName !== "!") a.push(node);
- return a;
- };
+ Object.extend(Selector.handlers, {
+ // IE returns comment nodes on getElementsByTagName("*").
+ // Filter them out.
+ concat: function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ if (node.tagName !== "!") a.push(node);
+ return a;
+ },
+
+ // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
+ unmark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node.removeAttribute('_countedByPrototype');
+ return nodes;
+ }
+ });
}
function $$() {
@@ -3850,9 +3844,9 @@ Object.extend(Event, (function() {
var cache = Event.cache;
function getEventID(element) {
- if (element._eventID) return element._eventID;
+ if (element._prototypeEventID) return element._prototypeEventID[0];
arguments.callee.id = arguments.callee.id || 1;
- return element._eventID = ++arguments.callee.id;
+ return element._prototypeEventID = [++arguments.callee.id];
}
function getDOMEventName(eventName) {
@@ -3880,7 +3874,7 @@ Object.extend(Event, (function() {
return false;
Event.extend(event);
- handler.call(element, event)
+ handler.call(element, event);
};
wrapper.handler = handler;
@@ -3962,11 +3956,12 @@ Object.extend(Event, (function() {
if (element == document && document.createEvent && !element.dispatchEvent)
element = document.documentElement;
+ var event;
if (document.createEvent) {
- var event = document.createEvent("HTMLEvents");
+ event = document.createEvent("HTMLEvents");
event.initEvent("dataavailable", true, true);
} else {
- var event = document.createEventObject();
+ event = document.createEventObject();
event.eventType = "ondataavailable";
}
@@ -3995,20 +3990,21 @@ Element.addMethods({
Object.extend(document, {
fire: Element.Methods.fire.methodize(),
observe: Element.Methods.observe.methodize(),
- stopObserving: Element.Methods.stopObserving.methodize()
+ stopObserving: Element.Methods.stopObserving.methodize(),
+ loaded: false
});
(function() {
/* Support for the DOMContentLoaded event is based on work by Dan Webb,
Matthias Miller, Dean Edwards and John Resig. */
- var timer, fired = false;
+ var timer;
function fireContentLoadedEvent() {
- if (fired) return;
+ if (document.loaded) return;
if (timer) window.clearInterval(timer);
document.fire("dom:loaded");
- fired = true;
+ document.loaded = true;
}
if (document.addEventListener) {
diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb
index 120bb4cc1f..77f19b36a6 100644
--- a/actionpack/lib/action_view/helpers/number_helper.rb
+++ b/actionpack/lib/action_view/helpers/number_helper.rb
@@ -22,14 +22,14 @@ module ActionView
# number_to_phone(1235551234, :country_code => 1) # => +1-123-555-1234
#
# number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".")
- # => +1.123.555.1234 x 1343
+ # => +1.123.555.1234 x 1343
def number_to_phone(number, options = {})
number = number.to_s.strip unless number.nil?
- options = options.stringify_keys
- area_code = options["area_code"] || nil
- delimiter = options["delimiter"] || "-"
- extension = options["extension"].to_s.strip || nil
- country_code = options["country_code"] || nil
+ options = options.symbolize_keys
+ area_code = options[:area_code] || nil
+ delimiter = options[:delimiter] || "-"
+ extension = options[:extension].to_s.strip || nil
+ country_code = options[:country_code] || nil
begin
str = ""
@@ -51,10 +51,10 @@ module ActionView
#
# ==== Options
# * <tt>:precision</tt> - Sets the level of precision (defaults to 2).
- # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$").
+ # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$").
# * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ",").
- # * <tt>:format</tt> - Sets the format of the output string (defaults to "%u%n"). The field types are:
+ # * <tt>:format</tt> - Sets the format of the output string (defaults to "%u%n"). The field types are:
#
# %u The currency unit
# %n The number
@@ -69,16 +69,25 @@ module ActionView
# number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "", :format => "%n %u")
# # => 1234567890,50 &pound;
def number_to_currency(number, options = {})
- options = options.stringify_keys
- precision = options["precision"] || 2
- unit = options["unit"] || "$"
- separator = precision > 0 ? options["separator"] || "." : ""
- delimiter = options["delimiter"] || ","
- format = options["format"] || "%u%n"
+ options.symbolize_keys!
+
+ defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
+ currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :raise => true) rescue {}
+ defaults = defaults.merge(currency)
+
+ precision = options[:precision] || defaults[:precision]
+ unit = options[:unit] || defaults[:unit]
+ separator = options[:separator] || defaults[:separator]
+ delimiter = options[:delimiter] || defaults[:delimiter]
+ format = options[:format] || defaults[:format]
+ separator = '' if precision == 0
begin
- parts = number_with_precision(number, precision).split('.')
- format.gsub(/%n/, number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s).gsub(/%u/, unit)
+ format.gsub(/%n/, number_with_precision(number,
+ :precision => precision,
+ :delimiter => delimiter,
+ :separator => separator)
+ ).gsub(/%u/, unit)
rescue
number
end
@@ -90,96 +99,192 @@ module ActionView
# ==== Options
# * <tt>:precision</tt> - Sets the level of precision (defaults to 3).
# * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to "").
#
# ==== Examples
- # number_to_percentage(100) # => 100.000%
- # number_to_percentage(100, :precision => 0) # => 100%
- #
- # number_to_percentage(302.24398923423, :precision => 5)
- # # => 302.24399%
+ # number_to_percentage(100) # => 100.000%
+ # number_to_percentage(100, :precision => 0) # => 100%
+ # number_to_percentage(1000, :delimiter => '.', :separator => ',') # => 1.000,000%
+ # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399%
def number_to_percentage(number, options = {})
- options = options.stringify_keys
- precision = options["precision"] || 3
- separator = options["separator"] || "."
+ options.symbolize_keys!
+
+ defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
+ percentage = I18n.translate(:'number.percentage.format', :locale => options[:locale], :raise => true) rescue {}
+ defaults = defaults.merge(percentage)
+
+ precision = options[:precision] || defaults[:precision]
+ separator = options[:separator] || defaults[:separator]
+ delimiter = options[:delimiter] || defaults[:delimiter]
begin
- number = number_with_precision(number, precision)
- parts = number.split('.')
- if parts.at(1).nil?
- parts[0] + "%"
- else
- parts[0] + separator + parts[1].to_s + "%"
- end
+ number_with_precision(number,
+ :precision => precision,
+ :separator => separator,
+ :delimiter => delimiter) + "%"
rescue
number
end
end
- # Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You
- # can customize the format using optional <em>delimiter</em> and <em>separator</em> parameters.
+ # Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You can
+ # customize the format in the +options+ hash.
#
# ==== Options
- # * <tt>delimiter</tt> - Sets the thousands delimiter (defaults to ",").
- # * <tt>separator</tt> - Sets the separator between the units (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ",").
+ # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
#
# ==== Examples
- # number_with_delimiter(12345678) # => 12,345,678
- # number_with_delimiter(12345678.05) # => 12,345,678.05
- # number_with_delimiter(12345678, ".") # => 12.345.678
- #
- # number_with_delimiter(98765432.98, " ", ",")
+ # number_with_delimiter(12345678) # => 12,345,678
+ # number_with_delimiter(12345678.05) # => 12,345,678.05
+ # number_with_delimiter(12345678, :delimiter => ".") # => 12.345.678
+ # number_with_delimiter(12345678, :seperator => ",") # => 12,345,678
+ # number_with_delimiter(98765432.98, :delimiter => " ", :separator => ",")
# # => 98 765 432,98
- def number_with_delimiter(number, delimiter=",", separator=".")
+ #
+ # You can still use <tt>number_with_delimiter</tt> with the old API that accepts the
+ # +delimiter+ as its optional second and the +separator+ as its
+ # optional third parameter:
+ # number_with_delimiter(12345678, " ") # => 12 345.678
+ # number_with_delimiter(12345678.05, ".", ",") # => 12.345.678,05
+ def number_with_delimiter(number, *args)
+ options = args.extract_options!
+ options.symbolize_keys!
+
+ defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
+
+ unless args.empty?
+ ActiveSupport::Deprecation.warn('number_with_delimiter takes an option hash ' +
+ 'instead of separate delimiter and precision arguments.', caller)
+ delimiter = args[0] || defaults[:delimiter]
+ separator = args[1] || defaults[:separator]
+ end
+
+ delimiter ||= (options[:delimiter] || defaults[:delimiter])
+ separator ||= (options[:separator] || defaults[:separator])
+
begin
parts = number.to_s.split('.')
parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
- parts.join separator
+ parts.join(separator)
rescue
number
end
end
- # Formats a +number+ with the specified level of +precision+ (e.g., 112.32 has a precision of 2). The default
- # level of precision is 3.
+ # Formats a +number+ with the specified level of <tt>:precision</tt> (e.g., 112.32 has a precision of 2).
+ # You can customize the format in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:precision</tt> - Sets the level of precision (defaults to 3).
+ # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to "").
#
# ==== Examples
- # number_with_precision(111.2345) # => 111.235
- # number_with_precision(111.2345, 2) # => 111.23
- # number_with_precision(13, 5) # => 13.00000
- # number_with_precision(389.32314, 0) # => 389
- def number_with_precision(number, precision=3)
- "%01.#{precision}f" % ((Float(number) * (10 ** precision)).round.to_f / 10 ** precision)
- rescue
- number
+ # number_with_precision(111.2345) # => 111.235
+ # number_with_precision(111.2345, :precision => 2) # => 111.23
+ # number_with_precision(13, :precision => 5) # => 13.00000
+ # number_with_precision(389.32314, :precision => 0) # => 389
+ # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.')
+ # # => 1.111,23
+ #
+ # You can still use <tt>number_with_precision</tt> with the old API that accepts the
+ # +precision+ as its optional second parameter:
+ # number_with_precision(number_with_precision(111.2345, 2) # => 111.23
+ def number_with_precision(number, *args)
+ options = args.extract_options!
+ options.symbolize_keys!
+
+ defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
+ precision_defaults = I18n.translate(:'number.precision.format', :locale => options[:locale],
+ :raise => true) rescue {}
+ defaults = defaults.merge(precision_defaults)
+
+ unless args.empty?
+ ActiveSupport::Deprecation.warn('number_with_precision takes an option hash ' +
+ 'instead of a separate precision argument.', caller)
+ precision = args[0] || defaults[:precision]
+ end
+
+ precision ||= (options[:precision] || defaults[:precision])
+ separator ||= (options[:separator] || defaults[:separator])
+ delimiter ||= (options[:delimiter] || defaults[:delimiter])
+
+ begin
+ rounded_number = (Float(number) * (10 ** precision)).round.to_f / 10 ** precision
+ number_with_delimiter("%01.#{precision}f" % rounded_number,
+ :separator => separator,
+ :delimiter => delimiter)
+ rescue
+ number
+ end
end
+ STORAGE_UNITS = %w( Bytes KB MB GB TB ).freeze
+
# Formats the bytes in +size+ into a more understandable representation
- # (e.g., giving it 1500 yields 1.5 KB). This method is useful for
+ # (e.g., giving it 1500 yields 1.5 KB). This method is useful for
# reporting file sizes to users. This method returns nil if
- # +size+ cannot be converted into a number. You can change the default
- # precision of 1 using the precision parameter +precision+.
+ # +size+ cannot be converted into a number. You can customize the
+ # format in the +options+ hash.
+ #
+ # ==== Options
+ # * <tt>:precision</tt> - Sets the level of precision (defaults to 1).
+ # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to "").
#
# ==== Examples
- # number_to_human_size(123) # => 123 Bytes
- # number_to_human_size(1234) # => 1.2 KB
- # number_to_human_size(12345) # => 12.1 KB
- # number_to_human_size(1234567) # => 1.2 MB
- # number_to_human_size(1234567890) # => 1.1 GB
- # number_to_human_size(1234567890123) # => 1.1 TB
+ # number_to_human_size(123) # => 123 Bytes
+ # number_to_human_size(1234) # => 1.2 KB
+ # number_to_human_size(12345) # => 12.1 KB
+ # number_to_human_size(1234567) # => 1.2 MB
+ # number_to_human_size(1234567890) # => 1.1 GB
+ # number_to_human_size(1234567890123) # => 1.1 TB
+ # number_to_human_size(1234567, :precision => 2) # => 1.18 MB
+ # number_to_human_size(483989, :precision => 0) # => 473 KB
+ # number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,18 MB
+ #
+ # You can still use <tt>number_to_human_size</tt> with the old API that accepts the
+ # +precision+ as its optional second parameter:
# number_to_human_size(1234567, 2) # => 1.18 MB
- # number_to_human_size(483989, 0) # => 4 MB
- def number_to_human_size(size, precision=1)
- size = Kernel.Float(size)
- case
- when size.to_i == 1; "1 Byte"
- when size < 1.kilobyte; "%d Bytes" % size
- when size < 1.megabyte; "%.#{precision}f KB" % (size / 1.0.kilobyte)
- when size < 1.gigabyte; "%.#{precision}f MB" % (size / 1.0.megabyte)
- when size < 1.terabyte; "%.#{precision}f GB" % (size / 1.0.gigabyte)
- else "%.#{precision}f TB" % (size / 1.0.terabyte)
- end.sub(/([0-9]\.\d*?)0+ /, '\1 ' ).sub(/\. /,' ')
- rescue
- nil
+ # number_to_human_size(483989, 0) # => 473 KB
+ def number_to_human_size(number, *args)
+ return number.nil? ? nil : pluralize(number.to_i, "Byte") if number.to_i < 1024
+
+ options = args.extract_options!
+ options.symbolize_keys!
+
+ defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
+ human = I18n.translate(:'number.human.format', :locale => options[:locale], :raise => true) rescue {}
+ defaults = defaults.merge(human)
+
+ unless args.empty?
+ ActiveSupport::Deprecation.warn('number_to_human_size takes an option hash ' +
+ 'instead of a separate precision argument.', caller)
+ precision = args[0] || defaults[:precision]
+ end
+
+ precision ||= (options[:precision] || defaults[:precision])
+ separator ||= (options[:separator] || defaults[:separator])
+ delimiter ||= (options[:delimiter] || defaults[:delimiter])
+
+ max_exp = STORAGE_UNITS.size - 1
+ number = Float(number)
+ exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024
+ exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
+ number /= 1024 ** exponent
+ unit = STORAGE_UNITS[exponent]
+
+ begin
+ escaped_separator = Regexp.escape(separator)
+ number_with_precision(number,
+ :precision => precision,
+ :separator => separator,
+ :delimiter => delimiter
+ ).sub(/(\d)(#{escaped_separator}[1-9]*)?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + " #{unit}"
+ rescue
+ number
+ end
end
end
end
diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb
index d0c281c803..ff83494e94 100644
--- a/actionpack/lib/action_view/helpers/prototype_helper.rb
+++ b/actionpack/lib/action_view/helpers/prototype_helper.rb
@@ -3,25 +3,25 @@ require 'set'
module ActionView
module Helpers
# Prototype[http://www.prototypejs.org/] is a JavaScript library that provides
- # DOM[http://en.wikipedia.org/wiki/Document_Object_Model] manipulation,
+ # DOM[http://en.wikipedia.org/wiki/Document_Object_Model] manipulation,
# Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php]
- # functionality, and more traditional object-oriented facilities for JavaScript.
+ # functionality, and more traditional object-oriented facilities for JavaScript.
# This module provides a set of helpers to make it more convenient to call
- # functions from Prototype using Rails, including functionality to call remote
- # Rails methods (that is, making a background request to a Rails action) using Ajax.
- # This means that you can call actions in your controllers without
- # reloading the page, but still update certain parts of it using
+ # functions from Prototype using Rails, including functionality to call remote
+ # Rails methods (that is, making a background request to a Rails action) using Ajax.
+ # This means that you can call actions in your controllers without
+ # reloading the page, but still update certain parts of it using
# injections into the DOM. A common use case is having a form that adds
# a new element to a list without reloading the page or updating a shopping
# cart total when a new item is added.
#
# == Usage
- # To be able to use these helpers, you must first include the Prototype
- # JavaScript framework in your pages.
+ # To be able to use these helpers, you must first include the Prototype
+ # JavaScript framework in your pages.
#
# javascript_include_tag 'prototype'
#
- # (See the documentation for
+ # (See the documentation for
# ActionView::Helpers::JavaScriptHelper for more information on including
# this and other JavaScript files in your Rails templates.)
#
@@ -29,7 +29,7 @@ module ActionView
#
# link_to_remote "Add to cart",
# :url => { :action => "add", :id => product.id },
- # :update => { :success => "cart", :failure => "error" }
+ # :update => { :success => "cart", :failure => "error" }
#
# ...through a form...
#
@@ -50,8 +50,8 @@ module ActionView
# :update => :hits,
# :with => 'query'
# %>
- #
- # As you can see, there are numerous ways to use Prototype's Ajax functions (and actually more than
+ #
+ # As you can see, there are numerous ways to use Prototype's Ajax functions (and actually more than
# are listed here); check out the documentation for each method to find out more about its usage and options.
#
# === Common Options
@@ -61,9 +61,9 @@ module ActionView
#
# == Designing your Rails actions for Ajax
# When building your action handlers (that is, the Rails actions that receive your background requests), it's
- # important to remember a few things. First, whatever your action would normall return to the browser, it will
+ # important to remember a few things. First, whatever your action would normally return to the browser, it will
# return to the Ajax call. As such, you typically don't want to render with a layout. This call will cause
- # the layout to be transmitted back to your page, and, if you have a full HTML/CSS, will likely mess a lot of things up.
+ # the layout to be transmitted back to your page, and, if you have a full HTML/CSS, will likely mess a lot of things up.
# You can turn the layout off on particular actions by doing the following:
#
# class SiteController < ActionController::Base
@@ -74,8 +74,8 @@ module ActionView
#
# render :layout => false
#
- # You can tell the type of request from within your action using the <tt>request.xhr?</tt> (XmlHttpRequest, the
- # method that Ajax uses to make background requests) method.
+ # You can tell the type of request from within your action using the <tt>request.xhr?</tt> (XmlHttpRequest, the
+ # method that Ajax uses to make background requests) method.
# def name
# # Is this an XmlHttpRequest request?
# if (request.xhr?)
@@ -93,7 +93,7 @@ module ActionView
#
# Dropping this in your ApplicationController turns the layout off for every request that is an "xhr" request.
#
- # If you are just returning a little data or don't want to build a template for your output, you may opt to simply
+ # If you are just returning a little data or don't want to build a template for your output, you may opt to simply
# render text output, like this:
#
# render :text => 'Return this from my method!'
@@ -103,7 +103,7 @@ module ActionView
#
# == Updating multiple elements
# See JavaScriptGenerator for information on updating multiple elements
- # on the page in an Ajax response.
+ # on the page in an Ajax response.
module PrototypeHelper
unless const_defined? :CALLBACKS
CALLBACKS = Set.new([ :uninitialized, :loading, :loaded,
@@ -111,67 +111,67 @@ module ActionView
(100..599).to_a)
AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url,
:asynchronous, :method, :insertion, :position,
- :form, :with, :update, :script ]).merge(CALLBACKS)
+ :form, :with, :update, :script, :type ]).merge(CALLBACKS)
end
- # Returns a link to a remote action defined by <tt>options[:url]</tt>
- # (using the url_for format) that's called in the background using
+ # Returns a link to a remote action defined by <tt>options[:url]</tt>
+ # (using the url_for format) that's called in the background using
# XMLHttpRequest. The result of that request can then be inserted into a
- # DOM object whose id can be specified with <tt>options[:update]</tt>.
+ # DOM object whose id can be specified with <tt>options[:update]</tt>.
# Usually, the result would be a partial prepared by the controller with
- # render :partial.
+ # render :partial.
#
# Examples:
- # # Generates: <a href="#" onclick="new Ajax.Updater('posts', '/blog/destroy/3', {asynchronous:true, evalScripts:true});
+ # # Generates: <a href="#" onclick="new Ajax.Updater('posts', '/blog/destroy/3', {asynchronous:true, evalScripts:true});
# # return false;">Delete this post</a>
- # link_to_remote "Delete this post", :update => "posts",
+ # link_to_remote "Delete this post", :update => "posts",
# :url => { :action => "destroy", :id => post.id }
#
- # # Generates: <a href="#" onclick="new Ajax.Updater('emails', '/mail/list_emails', {asynchronous:true, evalScripts:true});
+ # # Generates: <a href="#" onclick="new Ajax.Updater('emails', '/mail/list_emails', {asynchronous:true, evalScripts:true});
# # return false;"><img alt="Refresh" src="/images/refresh.png?" /></a>
- # link_to_remote(image_tag("refresh"), :update => "emails",
+ # link_to_remote(image_tag("refresh"), :update => "emails",
# :url => { :action => "list_emails" })
- #
+ #
# You can override the generated HTML options by specifying a hash in
# <tt>options[:html]</tt>.
- #
+ #
# link_to_remote "Delete this post", :update => "posts",
- # :url => post_url(@post), :method => :delete,
- # :html => { :class => "destructive" }
+ # :url => post_url(@post), :method => :delete,
+ # :html => { :class => "destructive" }
#
# You can also specify a hash for <tt>options[:update]</tt> to allow for
- # easy redirection of output to an other DOM element if a server-side
+ # easy redirection of output to an other DOM element if a server-side
# error occurs:
#
# Example:
- # # Generates: <a href="#" onclick="new Ajax.Updater({success:'posts',failure:'error'}, '/blog/destroy/5',
+ # # Generates: <a href="#" onclick="new Ajax.Updater({success:'posts',failure:'error'}, '/blog/destroy/5',
# # {asynchronous:true, evalScripts:true}); return false;">Delete this post</a>
# link_to_remote "Delete this post",
# :url => { :action => "destroy", :id => post.id },
# :update => { :success => "posts", :failure => "error" }
#
- # Optionally, you can use the <tt>options[:position]</tt> parameter to
- # influence how the target DOM element is updated. It must be one of
+ # Optionally, you can use the <tt>options[:position]</tt> parameter to
+ # influence how the target DOM element is updated. It must be one of
# <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>.
#
# The method used is by default POST. You can also specify GET or you
# can simulate PUT or DELETE over POST. All specified with <tt>options[:method]</tt>
#
# Example:
- # # Generates: <a href="#" onclick="new Ajax.Request('/person/4', {asynchronous:true, evalScripts:true, method:'delete'});
+ # # Generates: <a href="#" onclick="new Ajax.Request('/person/4', {asynchronous:true, evalScripts:true, method:'delete'});
# # return false;">Destroy</a>
# link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete
#
- # By default, these remote requests are processed asynchronous during
- # which various JavaScript callbacks can be triggered (for progress
- # indicators and the likes). All callbacks get access to the
- # <tt>request</tt> object, which holds the underlying XMLHttpRequest.
+ # By default, these remote requests are processed asynchronous during
+ # which various JavaScript callbacks can be triggered (for progress
+ # indicators and the likes). All callbacks get access to the
+ # <tt>request</tt> object, which holds the underlying XMLHttpRequest.
#
# To access the server response, use <tt>request.responseText</tt>, to
# find out the HTTP status, use <tt>request.status</tt>.
#
# Example:
- # # Generates: <a href="#" onclick="new Ajax.Request('/words/undo?n=33', {asynchronous:true, evalScripts:true,
+ # # Generates: <a href="#" onclick="new Ajax.Request('/words/undo?n=33', {asynchronous:true, evalScripts:true,
# # onComplete:function(request){undoRequestCompleted(request)}}); return false;">hello</a>
# word = 'hello'
# link_to_remote word,
@@ -180,43 +180,43 @@ module ActionView
#
# The callbacks that may be specified are (in order):
#
- # <tt>:loading</tt>:: Called when the remote document is being
+ # <tt>:loading</tt>:: Called when the remote document is being
# loaded with data by the browser.
# <tt>:loaded</tt>:: Called when the browser has finished loading
# the remote document.
- # <tt>:interactive</tt>:: Called when the user can interact with the
- # remote document, even though it has not
+ # <tt>:interactive</tt>:: Called when the user can interact with the
+ # remote document, even though it has not
# finished loading.
# <tt>:success</tt>:: Called when the XMLHttpRequest is completed,
# and the HTTP status code is in the 2XX range.
# <tt>:failure</tt>:: Called when the XMLHttpRequest is completed,
# and the HTTP status code is not in the 2XX
# range.
- # <tt>:complete</tt>:: Called when the XMLHttpRequest is complete
- # (fires after success/failure if they are
+ # <tt>:complete</tt>:: Called when the XMLHttpRequest is complete
+ # (fires after success/failure if they are
# present).
- #
- # You can further refine <tt>:success</tt> and <tt>:failure</tt> by
+ #
+ # You can further refine <tt>:success</tt> and <tt>:failure</tt> by
# adding additional callbacks for specific status codes.
#
# Example:
- # # Generates: <a href="#" onclick="new Ajax.Request('/testing/action', {asynchronous:true, evalScripts:true,
- # # on404:function(request){alert('Not found...? Wrong URL...?')},
+ # # Generates: <a href="#" onclick="new Ajax.Request('/testing/action', {asynchronous:true, evalScripts:true,
+ # # on404:function(request){alert('Not found...? Wrong URL...?')},
# # onFailure:function(request){alert('HTTP Error ' + request.status + '!')}}); return false;">hello</a>
# link_to_remote word,
# :url => { :action => "action" },
# 404 => "alert('Not found...? Wrong URL...?')",
# :failure => "alert('HTTP Error ' + request.status + '!')"
#
- # A status code callback overrides the success/failure handlers if
+ # A status code callback overrides the success/failure handlers if
# present.
#
# If you for some reason or another need synchronous processing (that'll
- # block the browser while the request is happening), you can specify
+ # block the browser while the request is happening), you can specify
# <tt>options[:type] = :synchronous</tt>.
#
# You can customize further browser side call logic by passing in
- # JavaScript code snippets via some optional parameters. In their order
+ # JavaScript code snippets via some optional parameters. In their order
# of use these are:
#
# <tt>:confirm</tt>:: Adds confirmation dialog.
@@ -228,7 +228,7 @@ module ActionView
# <tt>:after</tt>:: Called immediately after request was
# initiated and before <tt>:loading</tt>.
# <tt>:submit</tt>:: Specifies the DOM element ID that's used
- # as the parent of the form elements. By
+ # as the parent of the form elements. By
# default this is the current form, but
# it could just as well be the ID of a
# table row or any other DOM element.
@@ -238,10 +238,10 @@ module ActionView
# URL query string.
#
# Example:
- #
+ #
# :with => "'name=' + $('name').value"
#
- # You can generate a link that uses AJAX in the general case, while
+ # You can generate a link that uses AJAX in the general case, while
# degrading gracefully to plain link behavior in the absence of
# JavaScript by setting <tt>html_options[:href]</tt> to an alternate URL.
# Note the extra curly braces around the <tt>options</tt> hash separate
@@ -251,7 +251,7 @@ module ActionView
# link_to_remote "Delete this post",
# { :update => "posts", :url => { :action => "destroy", :id => post.id } },
# :href => url_for(:action => "destroy", :id => post.id)
- def link_to_remote(name, options = {}, html_options = nil)
+ def link_to_remote(name, options = {}, html_options = nil)
link_to_function(name, remote_function(options), html_options || options.delete(:html))
end
@@ -262,15 +262,15 @@ module ActionView
# and defining callbacks is the same as link_to_remote.
# Examples:
# # Call get_averages and put its results in 'avg' every 10 seconds
- # # Generates:
- # # new PeriodicalExecuter(function() {new Ajax.Updater('avg', '/grades/get_averages',
+ # # Generates:
+ # # new PeriodicalExecuter(function() {new Ajax.Updater('avg', '/grades/get_averages',
# # {asynchronous:true, evalScripts:true})}, 10)
# periodically_call_remote(:url => { :action => 'get_averages' }, :update => 'avg')
#
# # Call invoice every 10 seconds with the id of the customer
# # If it succeeds, update the invoice DIV; if it fails, update the error DIV
# # Generates:
- # # new PeriodicalExecuter(function() {new Ajax.Updater({success:'invoice',failure:'error'},
+ # # new PeriodicalExecuter(function() {new Ajax.Updater({success:'invoice',failure:'error'},
# # '/testing/invoice/16', {asynchronous:true, evalScripts:true})}, 10)
# periodically_call_remote(:url => { :action => 'invoice', :id => customer.id },
# :update => { :success => "invoice", :failure => "error" }
@@ -286,11 +286,11 @@ module ActionView
javascript_tag(code)
end
- # Returns a form tag that will submit using XMLHttpRequest in the
- # background instead of the regular reloading POST arrangement. Even
+ # Returns a form tag that will submit using XMLHttpRequest in the
+ # background instead of the regular reloading POST arrangement. Even
# though it's using JavaScript to serialize the form elements, the form
# submission will work just like a regular submission as viewed by the
- # receiving side (all elements available in <tt>params</tt>). The options for
+ # receiving side (all elements available in <tt>params</tt>). The options for
# specifying the target with <tt>:url</tt> and defining callbacks is the same as
# +link_to_remote+.
#
@@ -299,21 +299,21 @@ module ActionView
#
# Example:
# # Generates:
- # # <form action="/some/place" method="post" onsubmit="new Ajax.Request('',
+ # # <form action="/some/place" method="post" onsubmit="new Ajax.Request('',
# # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;">
- # form_remote_tag :html => { :action =>
+ # form_remote_tag :html => { :action =>
# url_for(:controller => "some", :action => "place") }
#
# The Hash passed to the <tt>:html</tt> key is equivalent to the options (2nd)
# argument in the FormTagHelper.form_tag method.
#
- # By default the fall-through action is the same as the one specified in
+ # By default the fall-through action is the same as the one specified in
# the <tt>:url</tt> (and the default method is <tt>:post</tt>).
#
# form_remote_tag also takes a block, like form_tag:
# # Generates:
- # # <form action="/" method="post" onsubmit="new Ajax.Request('/',
- # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)});
+ # # <form action="/" method="post" onsubmit="new Ajax.Request('/',
+ # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)});
# # return false;"> <div><input name="commit" type="submit" value="Save" /></div>
# # </form>
# <% form_remote_tag :url => '/posts' do -%>
@@ -323,19 +323,19 @@ module ActionView
options[:form] = true
options[:html] ||= {}
- options[:html][:onsubmit] =
- (options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") +
+ options[:html][:onsubmit] =
+ (options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") +
"#{remote_function(options)}; return false;"
form_tag(options[:html].delete(:action) || url_for(options[:url]), options[:html], &block)
end
- # Creates a form that will submit using XMLHttpRequest in the background
- # instead of the regular reloading POST arrangement and a scope around a
+ # Creates a form that will submit using XMLHttpRequest in the background
+ # instead of the regular reloading POST arrangement and a scope around a
# specific resource that is used as a base for questioning about
- # values for the fields.
+ # values for the fields.
#
- # === Resource
+ # === Resource
#
# Example:
# <% remote_form_for(@post) do |f| %>
@@ -348,7 +348,7 @@ module ActionView
# ...
# <% end %>
#
- # === Nested Resource
+ # === Nested Resource
#
# Example:
# <% remote_form_for([@post, @comment]) do |f| %>
@@ -387,23 +387,23 @@ module ActionView
concat('</form>')
end
alias_method :form_remote_for, :remote_form_for
-
+
# Returns a button input tag with the element name of +name+ and a value (i.e., display text) of +value+
# that will submit form using XMLHttpRequest in the background instead of a regular POST request that
- # reloads the page.
+ # reloads the page.
#
# # Create a button that submits to the create action
- # #
- # # Generates: <input name="create_btn" onclick="new Ajax.Request('/testing/create',
- # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
+ # #
+ # # Generates: <input name="create_btn" onclick="new Ajax.Request('/testing/create',
+ # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
# # return false;" type="button" value="Create" />
# <%= button_to_remote 'create_btn', 'Create', :url => { :action => 'create' } %>
#
# # Submit to the remote action update and update the DIV succeed or fail based
# # on the success or failure of the request
# #
- # # Generates: <input name="update_btn" onclick="new Ajax.Updater({success:'succeed',failure:'fail'},
- # # '/testing/update', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
+ # # Generates: <input name="update_btn" onclick="new Ajax.Updater({success:'succeed',failure:'fail'},
+ # # '/testing/update', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
# # return false;" type="button" value="Update" />
# <%= button_to_remote 'update_btn', 'Update', :url => { :action => 'update' },
# :update => { :success => "succeed", :failure => "fail" }
@@ -423,7 +423,7 @@ module ActionView
tag("input", options[:html], false)
end
alias_method :submit_to_remote, :button_to_remote
-
+
# Returns '<tt>eval(request.responseText)</tt>' which is the JavaScript function
# that +form_remote_tag+ can call in <tt>:complete</tt> to evaluate a multiple
# update return document using +update_element_function+ calls.
@@ -433,11 +433,11 @@ module ActionView
# Returns the JavaScript needed for a remote function.
# Takes the same arguments as link_to_remote.
- #
+ #
# Example:
- # # Generates: <select id="options" onchange="new Ajax.Updater('options',
+ # # Generates: <select id="options" onchange="new Ajax.Updater('options',
# # '/testing/update_options', {asynchronous:true, evalScripts:true})">
- # <select id="options" onchange="<%= remote_function(:update => "options",
+ # <select id="options" onchange="<%= remote_function(:update => "options",
# :url => { :action => :update_options }) %>">
# <option value="0">Hello</option>
# <option value="1">World</option>
@@ -455,7 +455,7 @@ module ActionView
update << "'#{options[:update]}'"
end
- function = update.empty? ?
+ function = update.empty? ?
"new Ajax.Request(" :
"new Ajax.Updater(#{update}, "
@@ -476,9 +476,9 @@ module ActionView
# callback when its contents have changed. The default callback is an
# Ajax call. By default the value of the observed field is sent as a
# parameter with the Ajax call.
- #
+ #
# Example:
- # # Generates: new Form.Element.Observer('suggest', 0.25, function(element, value) {new Ajax.Updater('suggest',
+ # # Generates: new Form.Element.Observer('suggest', 0.25, function(element, value) {new Ajax.Updater('suggest',
# # '/testing/find_suggestion', {asynchronous:true, evalScripts:true, parameters:'q=' + value})})
# <%= observe_field :suggest, :url => { :action => :find_suggestion },
# :frequency => 0.25,
@@ -500,14 +500,14 @@ module ActionView
# new Form.Element.Observer('glass', 1, function(element, value) {alert('Element changed')})
# The element parameter is the DOM element being observed, and the value is its value at the
# time the observer is triggered.
- #
+ #
# Additional options are:
# <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
# this field will be detected. Not setting this
# option at all or to a value equal to or less than
# zero will use event based observation instead of
# time based observation.
- # <tt>:update</tt>:: Specifies the DOM ID of the element whose
+ # <tt>:update</tt>:: Specifies the DOM ID of the element whose
# innerHTML should be updated with the
# XMLHttpRequest response text.
# <tt>:with</tt>:: A JavaScript expression specifying the parameters
@@ -518,7 +518,7 @@ module ActionView
# variable +value+.
#
# Examples
- #
+ #
# :with => "'my_custom_key=' + value"
# :with => "'person[name]=' + prompt('New name')"
# :with => "Form.Element.serialize('other-field')"
@@ -544,7 +544,7 @@ module ActionView
# observe_field 'book_title',
# :url => 'http://example.com/books/edit/1',
# :with => 'title'
- #
+ #
# # Sends params: {:book_title => 'Title of the book'} when the focus leaves
# # the input field.
# observe_field 'book_title',
@@ -558,7 +558,7 @@ module ActionView
build_observer('Form.Element.EventObserver', field_id, options)
end
end
-
+
# Observes the form with the DOM ID specified by +form_id+ and calls a
# callback when its contents have changed. The default callback is an
# Ajax call. By default all fields of the observed field are sent as
@@ -574,39 +574,57 @@ module ActionView
build_observer('Form.EventObserver', form_id, options)
end
end
-
- # All the methods were moved to GeneratorMethods so that
+
+ # All the methods were moved to GeneratorMethods so that
# #include_helpers_from_context has nothing to overwrite.
class JavaScriptGenerator #:nodoc:
def initialize(context, &block) #:nodoc:
@context, @lines = context, []
include_helpers_from_context
- @context.instance_exec(self, &block)
+ @context.with_output_buffer(@lines) do
+ @context.instance_exec(self, &block)
+ end
end
-
+
private
def include_helpers_from_context
- @context.extended_by.each do |mod|
- extend mod unless mod.name =~ /^ActionView::Helpers/
+ unless generator_methods_module = @context.instance_variable_get(:@__javascript_generator_methods__)
+ modules = @context.extended_by - ([ActionView::Helpers] + ActionView::Helpers.included_modules)
+
+ generator_methods_module = Module.new do
+ modules.each do |mod|
+ begin
+ include mod
+ rescue Exception => e
+ # HACK: Probably not a good idea to suppress these warnings
+ # AFAIK exceptions are only raised in while testing with mocha
+ # because the module does not like to be included into other
+ # non TestUnit classes
+ end
+ end
+ include GeneratorMethods
+ end
+ @context.instance_variable_set(:@__javascript_generator_methods__, generator_methods_module)
end
- extend GeneratorMethods
+
+ extend generator_methods_module
end
-
- # JavaScriptGenerator generates blocks of JavaScript code that allow you
- # to change the content and presentation of multiple DOM elements. Use
+
+ # JavaScriptGenerator generates blocks of JavaScript code that allow you
+ # to change the content and presentation of multiple DOM elements. Use
# this in your Ajax response bodies, either in a <script> tag or as plain
# JavaScript sent with a Content-type of "text/javascript".
#
- # Create new instances with PrototypeHelper#update_page or with
- # ActionController::Base#render, then call +insert_html+, +replace_html+,
- # +remove+, +show+, +hide+, +visual_effect+, or any other of the built-in
- # methods on the yielded generator in any order you like to modify the
- # content and appearance of the current page.
+ # Create new instances with PrototypeHelper#update_page or with
+ # ActionController::Base#render, then call +insert_html+, +replace_html+,
+ # +remove+, +show+, +hide+, +visual_effect+, or any other of the built-in
+ # methods on the yielded generator in any order you like to modify the
+ # content and appearance of the current page.
#
# Example:
#
# # Generates:
- # # new Insertion.Bottom("list", "<li>Some item</li>");
+ # # new Element.insert("list", { bottom: <li>Some item</li>" });
# # new Effect.Highlight("list");
# # ["status-indicator", "cancel-link"].each(Element.hide);
# update_page do |page|
@@ -614,12 +632,12 @@ module ActionView
# page.visual_effect :highlight, 'list'
# page.hide 'status-indicator', 'cancel-link'
# end
- #
+ #
#
# Helper methods can be used in conjunction with JavaScriptGenerator.
- # When a helper method is called inside an update block on the +page+
+ # When a helper method is called inside an update block on the +page+
# object, that method will also have access to a +page+ object.
- #
+ #
# Example:
#
# module ApplicationHelper
@@ -655,7 +673,7 @@ module ActionView
# end
# end
#
- # You can also use PrototypeHelper#update_page_tag instead of
+ # You can also use PrototypeHelper#update_page_tag instead of
# PrototypeHelper#update_page to wrap the generated JavaScript in a
# <script> tag.
module GeneratorMethods
@@ -668,7 +686,7 @@ module ActionView
end
end
end
-
+
# Returns a element reference by finding it through +id+ in the DOM. This element can then be
# used for further method calls. Examples:
#
@@ -689,31 +707,31 @@ module ActionView
JavaScriptElementProxy.new(self, ActionController::RecordIdentifier.dom_id(id))
end
end
-
- # Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
+
+ # Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
# expression as an argument to another JavaScriptGenerator method.
def literal(code)
ActiveSupport::JSON::Variable.new(code.to_s)
end
-
+
# Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
# used for further method calls. Examples:
#
# page.select('p') # => $$('p');
# page.select('p.welcome b').first # => $$('p.welcome b').first();
# page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();
- #
+ #
# You can also use prototype enumerations with the collection. Observe:
- #
+ #
# # Generates: $$('#items li').each(function(value) { value.hide(); });
# page.select('#items li').each do |value|
# value.hide
- # end
+ # end
#
- # Though you can call the block param anything you want, they are always rendered in the
+ # Though you can call the block param anything you want, they are always rendered in the
# javascript as 'value, index.' Other enumerations, like collect() return the last statement:
#
- # # Generates: var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
+ # # Generates: var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
# page.select('#items li').collect('hidden') do |item|
# item.hide
# end
@@ -721,13 +739,13 @@ module ActionView
def select(pattern)
JavaScriptElementCollectionProxy.new(self, pattern)
end
-
+
# Inserts HTML at the specified +position+ relative to the DOM element
# identified by the given +id+.
- #
+ #
# +position+ may be one of:
- #
- # <tt>:top</tt>:: HTML is inserted inside the element, before the
+ #
+ # <tt>:top</tt>:: HTML is inserted inside the element, before the
# element's existing content.
# <tt>:bottom</tt>:: HTML is inserted inside the element, after the
# element's existing content.
@@ -739,18 +757,18 @@ module ActionView
#
# # Insert the rendered 'navigation' partial just before the DOM
# # element with ID 'content'.
- # # Generates: new Insertion.Before("content", "-- Contents of 'navigation' partial --");
+ # # Generates: Element.insert("content", { before: "-- Contents of 'navigation' partial --" });
# page.insert_html :before, 'content', :partial => 'navigation'
#
# # Add a list item to the bottom of the <ul> with ID 'list'.
- # # Generates: new Insertion.Bottom("list", "<li>Last item</li>");
+ # # Generates: Element.insert("list", { bottom: "<li>Last item</li>" });
# page.insert_html :bottom, 'list', '<li>Last item</li>'
#
def insert_html(position, id, *options_for_render)
- insertion = position.to_s.camelize
- call "new Insertion.#{insertion}", id, render(*options_for_render)
+ content = javascript_object_for(render(*options_for_render))
+ record "Element.insert(\"#{id}\", { #{position.to_s.downcase}: #{content} });"
end
-
+
# Replaces the inner HTML of the DOM element with the given +id+.
#
# +options_for_render+ may be either a string of HTML to insert, or a hash
@@ -764,7 +782,7 @@ module ActionView
def replace_html(id, *options_for_render)
call 'Element.update', id, render(*options_for_render)
end
-
+
# Replaces the "outer HTML" (i.e., the entire element, not just its
# contents) of the DOM element with the given +id+.
#
@@ -786,7 +804,7 @@ module ActionView
# </div>
#
# # Insert a new person
- # #
+ # #
# # Generates: new Insertion.Bottom({object: "Matz", partial: "person"}, "");
# page.insert_html :bottom, :partial => 'person', :object => @person
#
@@ -798,7 +816,7 @@ module ActionView
def replace(id, *options_for_render)
call 'Element.replace', id, render(*options_for_render)
end
-
+
# Removes the DOM elements with the given +ids+ from the page.
#
# Example:
@@ -810,9 +828,9 @@ module ActionView
def remove(*ids)
loop_on_multiple_args 'Element.remove', ids
end
-
+
# Shows hidden DOM elements with the given +ids+.
- #
+ #
# Example:
#
# # Show a few people
@@ -822,7 +840,7 @@ module ActionView
def show(*ids)
loop_on_multiple_args 'Element.show', ids
end
-
+
# Hides the visible DOM elements with the given +ids+.
#
# Example:
@@ -832,9 +850,9 @@ module ActionView
# page.hide 'person_29', 'person_9', 'person_0'
#
def hide(*ids)
- loop_on_multiple_args 'Element.hide', ids
+ loop_on_multiple_args 'Element.hide', ids
end
-
+
# Toggles the visibility of the DOM elements with the given +ids+.
# Example:
#
@@ -844,9 +862,9 @@ module ActionView
# page.toggle 'person_14', 'person_12', 'person_23' # Shows the previously hidden elements
#
def toggle(*ids)
- loop_on_multiple_args 'Element.toggle', ids
+ loop_on_multiple_args 'Element.toggle', ids
end
-
+
# Displays an alert dialog with the given +message+.
#
# Example:
@@ -856,21 +874,21 @@ module ActionView
def alert(message)
call 'alert', message
end
-
+
# Redirects the browser to the given +location+ using JavaScript, in the same form as +url_for+.
#
# Examples:
#
# # Generates: window.location.href = "/mycontroller";
# page.redirect_to(:action => 'index')
- #
+ #
# # Generates: window.location.href = "/account/signup";
# page.redirect_to(:controller => 'account', :action => 'signup')
def redirect_to(location)
url = location.is_a?(String) ? location : @context.url_for(location)
record "window.location.href = #{url.inspect}"
end
-
+
# Reloads the browser's current +location+ using JavaScript
#
# Examples:
@@ -884,17 +902,17 @@ module ActionView
# Calls the JavaScript +function+, optionally with the given +arguments+.
#
# If a block is given, the block will be passed to a new JavaScriptGenerator;
- # the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt>
+ # the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt>
# and passed as the called function's final argument.
- #
+ #
# Examples:
#
# # Generates: Element.replace(my_element, "My content to replace with.")
# page.call 'Element.replace', 'my_element', "My content to replace with."
- #
+ #
# # Generates: alert('My message!')
# page.call 'alert', 'My message!'
- #
+ #
# # Generates:
# # my_method(function() {
# # $("one").show();
@@ -907,7 +925,7 @@ module ActionView
def call(function, *arguments, &block)
record "#{function}(#{arguments_for_call(arguments, block)})"
end
-
+
# Assigns the JavaScript +variable+ the given +value+.
#
# Examples:
@@ -918,13 +936,13 @@ module ActionView
# # Generates: record_count = 33;
# page.assign 'record_count', 33
#
- # # Generates: tabulated_total = 47
+ # # Generates: tabulated_total = 47
# page.assign 'tabulated_total', @total_from_cart
#
def assign(variable, value)
record "#{variable} = #{javascript_object_for(value)}"
end
-
+
# Writes raw JavaScript to the page.
#
# Example:
@@ -933,10 +951,10 @@ module ActionView
def <<(javascript)
@lines << javascript
end
-
+
# Executes the content of the block after a delay of +seconds+. Example:
#
- # # Generates:
+ # # Generates:
# # setTimeout(function() {
# # ;
# # new Effect.Fade("notice",{});
@@ -949,13 +967,13 @@ module ActionView
yield
record "}, #{(seconds * 1000).to_i})"
end
-
- # Starts a script.aculo.us visual effect. See
+
+ # Starts a script.aculo.us visual effect. See
# ActionView::Helpers::ScriptaculousHelper for more information.
def visual_effect(name, id = nil, options = {})
record @context.send(:visual_effect, name, id, options)
end
-
+
# Creates a script.aculo.us sortable element. Useful
# to recreate sortable elements after items get added
# or deleted.
@@ -963,66 +981,66 @@ module ActionView
def sortable(id, options = {})
record @context.send(:sortable_element_js, id, options)
end
-
+
# Creates a script.aculo.us draggable element.
# See ActionView::Helpers::ScriptaculousHelper for more information.
def draggable(id, options = {})
record @context.send(:draggable_element_js, id, options)
end
-
+
# Creates a script.aculo.us drop receiving element.
# See ActionView::Helpers::ScriptaculousHelper for more information.
def drop_receiving(id, options = {})
record @context.send(:drop_receiving_element_js, id, options)
end
-
+
private
def loop_on_multiple_args(method, ids)
- record(ids.size>1 ?
- "#{javascript_object_for(ids)}.each(#{method})" :
+ record(ids.size>1 ?
+ "#{javascript_object_for(ids)}.each(#{method})" :
"#{method}(#{ids.first.to_json})")
end
-
+
def page
self
end
-
+
def record(line)
returning line = "#{line.to_s.chomp.gsub(/\;\z/, '')};" do
self << line
end
end
-
+
def render(*options_for_render)
old_format = @context && @context.template_format
@context.template_format = :html if @context
- Hash === options_for_render.first ?
- @context.render(*options_for_render) :
+ Hash === options_for_render.first ?
+ @context.render(*options_for_render) :
options_for_render.first.to_s
ensure
@context.template_format = old_format if @context
end
-
+
def javascript_object_for(object)
object.respond_to?(:to_json) ? object.to_json : object.inspect
end
-
+
def arguments_for_call(arguments, block = nil)
arguments << block_to_function(block) if block
arguments.map { |argument| javascript_object_for(argument) }.join ', '
end
-
+
def block_to_function(block)
generator = self.class.new(@context, &block)
literal("function() { #{generator.to_s} }")
- end
+ end
def method_missing(method, *arguments)
JavaScriptProxy.new(self, method.to_s.camelize)
end
end
end
-
+
# Yields a JavaScriptGenerator and returns the generated JavaScript code.
# Use this to update multiple elements on a page in an Ajax response.
# See JavaScriptGenerator for more information.
@@ -1035,13 +1053,13 @@ module ActionView
def update_page(&block)
JavaScriptGenerator.new(@template, &block).to_s
end
-
+
# Works like update_page but wraps the generated JavaScript in a <script>
# tag. Use this to include generated JavaScript in an ERb template.
# See JavaScriptGenerator for more information.
#
# +html_options+ may be a hash of <script> attributes to be passed
- # to ActionView::Helpers::JavaScriptHelper#javascript_tag.
+ # to ActionView::Helpers::JavaScriptHelper#javascript_tag.
def update_page_tag(html_options = {}, &block)
javascript_tag update_page(&block), html_options
end
@@ -1049,10 +1067,10 @@ module ActionView
protected
def options_for_ajax(options)
js_options = build_callbacks(options)
-
+
js_options['asynchronous'] = options[:type] != :synchronous
js_options['method'] = method_option_to_s(options[:method]) if options[:method]
- js_options['insertion'] = "Insertion.#{options[:position].to_s.camelize}" if options[:position]
+ js_options['insertion'] = options[:position].to_s.downcase if options[:position]
js_options['evalScripts'] = options[:script].nil? || options[:script]
if options[:form]
@@ -1062,7 +1080,7 @@ module ActionView
elsif options[:with]
js_options['parameters'] = options[:with]
end
-
+
if protect_against_forgery? && !options[:form]
if js_options['parameters']
js_options['parameters'] << " + '&"
@@ -1071,14 +1089,14 @@ module ActionView
end
js_options['parameters'] << "#{request_forgery_protection_token}=' + encodeURIComponent('#{escape_javascript form_authenticity_token}')"
end
-
+
options_for_javascript(js_options)
end
- def method_option_to_s(method)
+ def method_option_to_s(method)
(method.is_a?(String) and !method.index("'").nil?) ? method : "'#{method}'"
end
-
+
def build_observer(klass, name, options = {})
if options[:with] && (options[:with] !~ /[\{=(.]/)
options[:with] = "'#{options[:with]}=' + encodeURIComponent(value)"
@@ -1095,7 +1113,7 @@ module ActionView
javascript << ")"
javascript_tag(javascript)
end
-
+
def build_callbacks(options)
callbacks = {}
options.each do |callback, code|
@@ -1108,7 +1126,7 @@ module ActionView
end
end
- # Converts chained method calls on DOM proxy elements into JavaScript chains
+ # Converts chained method calls on DOM proxy elements into JavaScript chains
class JavaScriptProxy < ActiveSupport::BasicObject #:nodoc:
def initialize(generator, root = nil)
@@ -1124,7 +1142,7 @@ module ActionView
call("#{method.to_s.camelize(:lower)}", *arguments, &block)
end
end
-
+
def call(function, *arguments, &block)
append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments, block)})")
self
@@ -1133,23 +1151,23 @@ module ActionView
def assign(variable, value)
append_to_function_chain!("#{variable} = #{@generator.send(:javascript_object_for, value)}")
end
-
+
def function_chain
@function_chain ||= @generator.instance_variable_get(:@lines)
end
-
+
def append_to_function_chain!(call)
function_chain[-1].chomp!(';')
function_chain[-1] += ".#{call};"
end
end
-
+
class JavaScriptElementProxy < JavaScriptProxy #:nodoc:
def initialize(generator, id)
@id = id
super(generator, "$(#{id.to_json})")
end
-
+
# Allows access of element attributes through +attribute+. Examples:
#
# page['foo']['style'] # => $('foo').style;
@@ -1160,11 +1178,11 @@ module ActionView
append_to_function_chain!(attribute)
self
end
-
+
def []=(variable, value)
assign(variable, value)
end
-
+
def replace_html(*options_for_render)
call 'update', @generator.send(:render, *options_for_render)
end
@@ -1172,11 +1190,11 @@ module ActionView
def replace(*options_for_render)
call 'replace', @generator.send(:render, *options_for_render)
end
-
+
def reload(options_for_replace = {})
replace(options_for_replace.merge({ :partial => @id.to_s }))
end
-
+
end
class JavaScriptVariableProxy < JavaScriptProxy #:nodoc:
@@ -1195,7 +1213,7 @@ module ActionView
def to_json(options = nil)
@variable
end
-
+
private
def append_to_function_chain!(call)
@generator << @variable if @empty
@@ -1213,7 +1231,7 @@ module ActionView
def initialize(generator, pattern)
super(generator, @pattern = pattern)
end
-
+
def each_slice(variable, number, &block)
if block
enumerate :eachSlice, :variable => variable, :method_args => [number], :yield_args => %w(value index), :return => true, &block
@@ -1222,18 +1240,18 @@ module ActionView
append_enumerable_function!("eachSlice(#{number.to_json});")
end
end
-
+
def grep(variable, pattern, &block)
enumerate :grep, :variable => variable, :return => true, :method_args => [pattern], :yield_args => %w(value index), &block
end
-
+
def in_groups_of(variable, number, fill_with = nil)
arguments = [number]
arguments << fill_with unless fill_with.nil?
add_variable_assignment!(variable)
append_enumerable_function!("inGroupsOf(#{arguments_for_call arguments});")
- end
-
+ end
+
def inject(variable, memo, &block)
enumerate :inject, :variable => variable, :method_args => [memo], :yield_args => %w(memo value index), :return => true, &block
end
@@ -1295,13 +1313,13 @@ module ActionView
function_chain.push("return #{function_chain.pop.chomp(';')};")
end
end
-
+
def append_enumerable_function!(call)
function_chain[-1].chomp!(';')
function_chain[-1] += ".#{call}"
end
end
-
+
class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\
def initialize(generator, pattern)
super(generator, "$$(#{pattern.to_json})")
diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb
index b0dacfe964..637caf203b 100644
--- a/actionpack/lib/action_view/helpers/sanitize_helper.rb
+++ b/actionpack/lib/action_view/helpers/sanitize_helper.rb
@@ -6,17 +6,13 @@ module ActionView
# The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
# These helper methods extend ActionView making them callable within your template files.
module SanitizeHelper
- def self.included(base)
- base.extend(ClassMethods)
- end
-
# This +sanitize+ helper will html encode all tags and strip all attributes that aren't specifically allowed.
# It also strips href/src tags with invalid protocols, like javascript: especially. It does its best to counter any
# tricks that hackers may use, like throwing in unicode/ascii/hex values to get past the javascript: filters. Check out
# the extensive test suite.
#
# <%= sanitize @article.body %>
- #
+ #
# You can add or remove tags/attributes if you want to customize it a bit. See ActionView::Base for full docs on the
# available options. You can add tags/attributes for single uses of +sanitize+ by passing either the <tt>:attributes</tt> or <tt>:tags</tt> options:
#
@@ -27,27 +23,27 @@ module ActionView
# Custom Use (only the mentioned tags and attributes are allowed, nothing else)
#
# <%= sanitize @article.body, :tags => %w(table tr td), :attributes => %w(id class style)
- #
+ #
# Add table tags to the default allowed tags
- #
+ #
# Rails::Initializer.run do |config|
# config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
# end
- #
+ #
# Remove tags to the default allowed tags
- #
+ #
# Rails::Initializer.run do |config|
# config.after_initialize do
# ActionView::Base.sanitized_allowed_tags.delete 'div'
# end
# end
- #
+ #
# Change allowed default attributes
- #
+ #
# Rails::Initializer.run do |config|
# config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style'
# end
- #
+ #
# Please note that sanitizing user-provided text does not guarantee that the
# resulting markup is valid (conforming to a document type) or even well-formed.
# The output may still contain e.g. unescaped '<', '>', '&' characters and
@@ -62,8 +58,8 @@ module ActionView
self.class.white_list_sanitizer.sanitize_css(style)
end
- # Strips all HTML tags from the +html+, including comments. This uses the
- # html-scanner tokenizer and so its HTML parsing ability is limited by
+ # Strips all HTML tags from the +html+, including comments. This uses the
+ # html-scanner tokenizer and so its HTML parsing ability is limited by
# that of html-scanner.
#
# ==== Examples
@@ -73,10 +69,10 @@ module ActionView
#
# strip_tags("<b>Bold</b> no more! <a href='more.html'>See more here</a>...")
# # => Bold no more! See more here...
- #
+ #
# strip_tags("<div id='top-bar'>Welcome to my website!</div>")
# # => Welcome to my website!
- def strip_tags(html)
+ def strip_tags(html)
self.class.full_sanitizer.sanitize(html)
end
@@ -96,21 +92,48 @@ module ActionView
end
module ClassMethods #:nodoc:
- def self.extended(base)
- class << base
- attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
-
- # we want these to be class methods on ActionView::Base, they'll get mattr_readers for these below.
- helper_def = [:sanitized_protocol_separator, :sanitized_uri_attributes, :sanitized_bad_tags, :sanitized_allowed_tags,
- :sanitized_allowed_attributes, :sanitized_allowed_css_properties, :sanitized_allowed_css_keywords,
- :sanitized_shorthand_css_properties, :sanitized_allowed_protocols, :sanitized_protocol_separator=].collect! do |prop|
- prop = prop.to_s
- "def #{prop}(#{:value if prop =~ /=$/}) white_list_sanitizer.#{prop.sub /sanitized_/, ''} #{:value if prop =~ /=$/} end"
- end.join("\n")
- eval helper_def
- end
- end
-
+ attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
+
+ def sanitized_protocol_separator
+ white_list_sanitizer.protocol_separator
+ end
+
+ def sanitized_uri_attributes
+ white_list_sanitizer.uri_attributes
+ end
+
+ def sanitized_bad_tags
+ white_list_sanitizer.bad_tags
+ end
+
+ def sanitized_allowed_tags
+ white_list_sanitizer.allowed_tags
+ end
+
+ def sanitized_allowed_attributes
+ white_list_sanitizer.allowed_attributes
+ end
+
+ def sanitized_allowed_css_properties
+ white_list_sanitizer.allowed_css_properties
+ end
+
+ def sanitized_allowed_css_keywords
+ white_list_sanitizer.allowed_css_keywords
+ end
+
+ def sanitized_shorthand_css_properties
+ white_list_sanitizer.shorthand_css_properties
+ end
+
+ def sanitized_allowed_protocols
+ white_list_sanitizer.allowed_protocols
+ end
+
+ def sanitized_protocol_separator=(value)
+ white_list_sanitizer.protocol_separator = value
+ end
+
# Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with
# any object that responds to +sanitize+.
#
@@ -184,7 +207,7 @@ module ActionView
HTML::WhiteListSanitizer.allowed_attributes.merge(attributes)
end
- # Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ heleprs.
+ # Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ helpers.
#
# Rails::Initializer.run do |config|
# config.action_view.sanitized_allowed_css_properties = 'expression'
diff --git a/actionpack/lib/action_view/helpers/scriptaculous_helper.rb b/actionpack/lib/action_view/helpers/scriptaculous_helper.rb
index c9b2761cb8..b938c1a801 100644
--- a/actionpack/lib/action_view/helpers/scriptaculous_helper.rb
+++ b/actionpack/lib/action_view/helpers/scriptaculous_helper.rb
@@ -193,7 +193,7 @@ module ActionView
#
# * <tt>:onDrop</tt> - Called when a +draggable_element+ is dropped onto
# this element. Override this callback with a JavaScript expression to
- # change the default drop behavour. Example:
+ # change the default drop behaviour. Example:
#
# :onDrop => "function(draggable_element, droppable_element, event) { alert('I like bananas') }"
#
diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb
index aeafd3906d..de08672d2d 100644
--- a/actionpack/lib/action_view/helpers/tag_helper.rb
+++ b/actionpack/lib/action_view/helpers/tag_helper.rb
@@ -64,7 +64,7 @@ module ActionView
# <% content_tag :div, :class => "strong" do -%>
# Hello world!
# <% end -%>
- # # => <div class="strong"><p>Hello world!</p></div>
+ # # => <div class="strong">Hello world!</div>
def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
if block_given?
options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
@@ -110,12 +110,18 @@ module ActionView
private
BLOCK_CALLED_FROM_ERB = 'defined? __in_erb_template'
- # Check whether we're called from an erb template.
- # We'd return a string in any other case, but erb <%= ... %>
- # can't take an <% end %> later on, so we have to use <% ... %>
- # and implicitly concat.
- def block_called_from_erb?(block)
- block && eval(BLOCK_CALLED_FROM_ERB, block)
+ if RUBY_VERSION < '1.9.0'
+ # Check whether we're called from an erb template.
+ # We'd return a string in any other case, but erb <%= ... %>
+ # can't take an <% end %> later on, so we have to use <% ... %>
+ # and implicitly concat.
+ def block_called_from_erb?(block)
+ block && eval(BLOCK_CALLED_FROM_ERB, block)
+ end
+ else
+ def block_called_from_erb?(block)
+ block && eval(BLOCK_CALLED_FROM_ERB, block.binding)
+ end
end
def content_tag_string(name, content, options, escape = true)
diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb
index 3e3452b615..f9096d0029 100644
--- a/actionpack/lib/action_view/helpers/text_helper.rb
+++ b/actionpack/lib/action_view/helpers/text_helper.rb
@@ -34,40 +34,69 @@ module ActionView
end
if RUBY_VERSION < '1.9'
- # If +text+ is longer than +length+, +text+ will be truncated to the length of
- # +length+ (defaults to 30) and the last characters will be replaced with the +truncate_string+
- # (defaults to "...").
+ # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
+ # (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...").
#
# ==== Examples
- # truncate("Once upon a time in a world far far away", 14)
- # # => Once upon a...
#
# truncate("Once upon a time in a world far far away")
# # => Once upon a time in a world f...
#
- # truncate("And they found that many people were sleeping better.", 25, "(clipped)")
+ # truncate("Once upon a time in a world far far away", :length => 14)
+ # # => Once upon a...
+ #
+ # truncate("And they found that many people were sleeping better.", :length => 25, "(clipped)")
# # => And they found that many (clipped)
#
+ # truncate("And they found that many people were sleeping better.", :omission => "... (continued)", :length => 15)
+ # # => And they found... (continued)
+ #
+ # You can still use <tt>truncate</tt> with the old API that accepts the
+ # +length+ as its optional second and the +ellipsis+ as its
+ # optional third parameter:
+ # truncate("Once upon a time in a world far far away", 14)
+ # # => Once upon a time in a world f...
+ #
# truncate("And they found that many people were sleeping better.", 15, "... (continued)")
# # => And they found... (continued)
- def truncate(text, length = 30, truncate_string = "...")
+ def truncate(text, *args)
+ options = args.extract_options!
+ unless args.empty?
+ ActiveSupport::Deprecation.warn('truncate takes an option hash instead of separate ' +
+ 'length and omission arguments', caller)
+
+ options[:length] = args[0] || 30
+ options[:omission] = args[1] || "..."
+ end
+ options.reverse_merge!(:length => 30, :omission => "...")
+
if text
- l = length - truncate_string.chars.length
+ l = options[:length] - options[:omission].chars.length
chars = text.chars
- (chars.length > length ? chars[0...l] + truncate_string : text).to_s
+ (chars.length > options[:length] ? chars[0...l] + options[:omission] : text).to_s
end
end
else
- def truncate(text, length = 30, truncate_string = "...") #:nodoc:
+ def truncate(text, *args) #:nodoc:
+ options = args.extract_options!
+ unless args.empty?
+ ActiveSupport::Deprecation.warn('truncate takes an option hash instead of separate ' +
+ 'length and omission arguments', caller)
+
+ options[:length] = args[0] || 30
+ options[:omission] = args[1] || "..."
+ end
+ options.reverse_merge!(:length => 30, :omission => "...")
+
if text
- l = length - truncate_string.length
- (text.length > length ? text[0...l] + truncate_string : text).to_s
+ l = options[:length].to_i - options[:omission].length
+ (text.length > options[:length].to_i ? text[0...l] + options[:omission] : text).to_s
end
end
end
# Highlights one or more +phrases+ everywhere in +text+ by inserting it into
- # a +highlighter+ string. The highlighter can be specialized by passing +highlighter+
+ # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
# as a single-quoted string with \1 where the phrase is to be inserted (defaults to
# '<strong class="highlight">\1</strong>')
#
@@ -78,52 +107,75 @@ module ActionView
# highlight('You searched for: ruby, rails, dhh', 'actionpack')
# # => You searched for: ruby, rails, dhh
#
- # highlight('You searched for: rails', ['for', 'rails'], '<em>\1</em>')
+ # highlight('You searched for: rails', ['for', 'rails'], :highlighter => '<em>\1</em>')
# # => You searched <em>for</em>: <em>rails</em>
#
- # highlight('You searched for: rails', 'rails', "<a href='search?q=\1'>\1</a>")
- # # => You searched for: <a href='search?q=rails>rails</a>
- def highlight(text, phrases, highlighter = '<strong class="highlight">\1</strong>')
+ # highlight('You searched for: rails', 'rails', :highlighter => '<a href="search?q=\1">\1</a>')
+ # # => You searched for: <a href="search?q=rails">rails</a>
+ #
+ # You can still use <tt>highlight</tt> with the old API that accepts the
+ # +highlighter+ as its optional third parameter:
+ # highlight('You searched for: rails', 'rails', '<a href="search?q=\1">\1</a>') # => You searched for: <a href="search?q=rails">rails</a>
+ def highlight(text, phrases, *args)
+ options = args.extract_options!
+ unless args.empty?
+ options[:highlighter] = args[0] || '<strong class="highlight">\1</strong>'
+ end
+ options.reverse_merge!(:highlighter => '<strong class="highlight">\1</strong>')
+
if text.blank? || phrases.blank?
text
else
match = Array(phrases).map { |p| Regexp.escape(p) }.join('|')
- text.gsub(/(#{match})/i, highlighter)
+ text.gsub(/(#{match})/i, options[:highlighter])
end
end
if RUBY_VERSION < '1.9'
# Extracts an excerpt from +text+ that matches the first instance of +phrase+.
- # The +radius+ expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
- # defined in +radius+ (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
- # then the +excerpt_string+ will be prepended/appended accordingly. The resulting string will be stripped in any case.
- # If the +phrase+ isn't found, nil is returned.
+ # The <tt>:radius</tt> option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
+ # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
+ # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. The resulting string
+ # will be stripped in any case. If the +phrase+ isn't found, nil is returned.
#
# ==== Examples
- # excerpt('This is an example', 'an', 5)
- # # => "...s is an exam..."
+ # excerpt('This is an example', 'an', :radius => 5)
+ # # => ...s is an exam...
#
- # excerpt('This is an example', 'is', 5)
- # # => "This is a..."
+ # excerpt('This is an example', 'is', :radius => 5)
+ # # => This is a...
#
# excerpt('This is an example', 'is')
- # # => "This is an example"
+ # # => This is an example
#
- # excerpt('This next thing is an example', 'ex', 2)
- # # => "...next..."
+ # excerpt('This next thing is an example', 'ex', :radius => 2)
+ # # => ...next...
#
- # excerpt('This is also an example', 'an', 8, '<chop> ')
- # # => "<chop> is also an example"
- def excerpt(text, phrase, radius = 100, excerpt_string = "...")
+ # excerpt('This is also an example', 'an', :radius => 8, :omission => '<chop> ')
+ # # => <chop> is also an example
+ #
+ # You can still use <tt>excerpt</tt> with the old API that accepts the
+ # +radius+ as its optional third and the +ellipsis+ as its
+ # optional forth parameter:
+ # excerpt('This is an example', 'an', 5) # => ...s is an exam...
+ # excerpt('This is also an example', 'an', 8, '<chop> ') # => <chop> is also an example
+ def excerpt(text, phrase, *args)
+ options = args.extract_options!
+ unless args.empty?
+ options[:radius] = args[0] || 100
+ options[:omission] = args[1] || "..."
+ end
+ options.reverse_merge!(:radius => 100, :omission => "...")
+
if text && phrase
phrase = Regexp.escape(phrase)
if found_pos = text.chars =~ /(#{phrase})/i
- start_pos = [ found_pos - radius, 0 ].max
- end_pos = [ [ found_pos + phrase.chars.length + radius - 1, 0].max, text.chars.length ].min
+ start_pos = [ found_pos - options[:radius], 0 ].max
+ end_pos = [ [ found_pos + phrase.chars.length + options[:radius] - 1, 0].max, text.chars.length ].min
- prefix = start_pos > 0 ? excerpt_string : ""
- postfix = end_pos < text.chars.length - 1 ? excerpt_string : ""
+ prefix = start_pos > 0 ? options[:omission] : ""
+ postfix = end_pos < text.chars.length - 1 ? options[:omission] : ""
prefix + text.chars[start_pos..end_pos].strip + postfix
else
@@ -132,16 +184,23 @@ module ActionView
end
end
else
- def excerpt(text, phrase, radius = 100, excerpt_string = "...") #:nodoc:
+ def excerpt(text, phrase, *args) #:nodoc:
+ options = args.extract_options!
+ unless args.empty?
+ options[:radius] = args[0] || 100
+ options[:omission] = args[1] || "..."
+ end
+ options.reverse_merge!(:radius => 100, :omission => "...")
+
if text && phrase
phrase = Regexp.escape(phrase)
if found_pos = text =~ /(#{phrase})/i
- start_pos = [ found_pos - radius, 0 ].max
- end_pos = [ [ found_pos + phrase.length + radius - 1, 0].max, text.length ].min
+ start_pos = [ found_pos - options[:radius], 0 ].max
+ end_pos = [ [ found_pos + phrase.length + options[:radius] - 1, 0].max, text.length ].min
- prefix = start_pos > 0 ? excerpt_string : ""
- postfix = end_pos < text.length - 1 ? excerpt_string : ""
+ prefix = start_pos > 0 ? options[:omission] : ""
+ postfix = end_pos < text.length - 1 ? options[:omission] : ""
prefix + text[start_pos..end_pos].strip + postfix
else
@@ -176,20 +235,31 @@ module ActionView
# (which is 80 by default).
#
# ==== Examples
- # word_wrap('Once upon a time', 4)
- # # => Once\nupon\na\ntime
- #
- # word_wrap('Once upon a time', 8)
- # # => Once upon\na time
#
# word_wrap('Once upon a time')
# # => Once upon a time
#
- # word_wrap('Once upon a time', 1)
+ # word_wrap('Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding a successor to the throne turned out to be more trouble than anyone could have imagined...')
+ # # => Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding\n a successor to the throne turned out to be more trouble than anyone could have\n imagined...
+ #
+ # word_wrap('Once upon a time', :line_width => 8)
+ # # => Once upon\na time
+ #
+ # word_wrap('Once upon a time', :line_width => 1)
# # => Once\nupon\na\ntime
- def word_wrap(text, line_width = 80)
+ #
+ # You can still use <tt>word_wrap</tt> with the old API that accepts the
+ # +line_width+ as its optional second parameter:
+ # word_wrap('Once upon a time', 8) # => Once upon\na time
+ def word_wrap(text, *args)
+ options = args.extract_options!
+ unless args.blank?
+ options[:line_width] = args[0] || 80
+ end
+ options.reverse_merge!(:line_width => 80)
+
text.split("\n").collect do |line|
- line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
+ line.length > options[:line_width] ? line.gsub(/(.{1,#{options[:line_width]}})(\s+|$)/, "\\1\n").strip : line
end * "\n"
end
@@ -336,12 +406,32 @@ module ActionView
# # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.m...</a>.
# Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>."
#
- def auto_link(text, link = :all, href_options = {}, &block)
+ #
+ # You can still use <tt>auto_link</tt> with the old API that accepts the
+ # +link+ as its optional second parameter and the +html_options+ hash
+ # as its optional third parameter:
+ # post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com."
+ # auto_link(post_body, :urls) # => Once upon\na time
+ # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\">http://www.myblog.com</a>.
+ # Please e-mail me at me@email.com."
+ #
+ # auto_link(post_body, :all, :target => "_blank") # => Once upon\na time
+ # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.myblog.com</a>.
+ # Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>."
+ def auto_link(text, *args, &block)#link = :all, href_options = {}, &block)
return '' if text.blank?
- case link
- when :all then auto_link_email_addresses(auto_link_urls(text, href_options, &block), &block)
- when :email_addresses then auto_link_email_addresses(text, &block)
- when :urls then auto_link_urls(text, href_options, &block)
+
+ options = args.size == 2 ? {} : args.extract_options! # this is necessary because the old auto_link API has a Hash as its last parameter
+ unless args.empty?
+ options[:link] = args[0] || :all
+ options[:html] = args[1] || {}
+ end
+ options.reverse_merge!(:link => :all, :html => {})
+
+ case options[:link].to_sym
+ when :all then auto_link_email_addresses(auto_link_urls(text, options[:html], &block), &block)
+ when :email_addresses then auto_link_email_addresses(text, &block)
+ when :urls then auto_link_urls(text, options[:html], &block)
end
end
@@ -468,7 +558,7 @@ module ActionView
[-\w]+ # subdomain or domain
(?:\.[-\w]+)* # remaining subdomains or domain
(?::\d+)? # port
- (?:/(?:(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$]))+)?)* # path
+ (?:/(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$]))*)* # path
(?:\?[\w\+@%&=.;-]+)? # query string
(?:\#[\w\-]*)? # trailing anchor
)
@@ -477,8 +567,8 @@ module ActionView
# Turns all urls into clickable links. If a block is given, each url
# is yielded and the result is used as the link text.
- def auto_link_urls(text, href_options = {})
- extra_options = tag_options(href_options.stringify_keys) || ""
+ def auto_link_urls(text, html_options = {})
+ extra_options = tag_options(html_options.stringify_keys) || ""
text.gsub(AUTO_LINK_RE) do
all, a, b, c, d = $&, $1, $2, $3, $4
if a =~ /<a\s/i # don't replace URL's that are already linked
@@ -508,4 +598,4 @@ module ActionView
end
end
end
-end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb
new file mode 100644
index 0000000000..de4c1d7689
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/translation_helper.rb
@@ -0,0 +1,22 @@
+require 'action_view/helpers/tag_helper'
+
+module ActionView
+ module Helpers
+ module TranslationHelper
+ def translate(*args)
+ args << args.extract_options!.merge(:raise => true)
+ I18n.translate *args
+
+ rescue I18n::MissingTranslationData => e
+ keys = I18n.send :normalize_translation_keys, e.locale, e.key, e.options[:scope]
+ content_tag('span', keys.join(', '), :class => 'translation_missing')
+ end
+ alias :t :translate
+
+ def localize(*args)
+ I18n.localize *args
+ end
+ alias :l :localize
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb
index e5178938fd..7ba42a3b72 100644
--- a/actionpack/lib/action_view/helpers/url_helper.rb
+++ b/actionpack/lib/action_view/helpers/url_helper.rb
@@ -3,8 +3,8 @@ require 'action_view/helpers/javascript_helper'
module ActionView
module Helpers #:nodoc:
# Provides a set of methods for making links and getting URLs that
- # depend on the routing subsystem (see ActionController::Routing).
- # This allows you to use the same format for links in views
+ # depend on the routing subsystem (see ActionController::Routing).
+ # This allows you to use the same format for links in views
# and controllers.
module UrlHelper
include JavaScriptHelper
@@ -33,8 +33,8 @@ module ActionView
#
# If you instead of a hash pass a record (like an Active Record or Active Resource) as the options parameter,
# you'll trigger the named route for that record. The lookup will happen on the name of the class. So passing
- # a Workshop object will attempt to use the workshop_path route. If you have a nested route, such as
- # admin_workshop_path you'll have to call that explicitly (it's impossible for url_for to guess that route).
+ # a Workshop object will attempt to use the workshop_path route. If you have a nested route, such as
+ # admin_workshop_path you'll have to call that explicitly (it's impossible for url_for to guess that route).
#
# ==== Examples
# <%= url_for(:action => 'index') %>
@@ -62,19 +62,33 @@ module ActionView
# <%= url_for(@workshop) %>
# # calls @workshop.to_s
# # => /workshops/5
+ #
+ # <%= url_for("http://www.example.com") %>
+ # # => http://www.example.com
+ #
+ # <%= url_for(:back) %>
+ # # if request.env["HTTP_REFERER"] is set to "http://www.example.com"
+ # # => http://www.example.com
+ #
+ # <%= url_for(:back) %>
+ # # if request.env["HTTP_REFERER"] is not set or is blank
+ # # => javascript:history.back()
def url_for(options = {})
options ||= {}
- case options
+ url = case options
+ when String
+ escape = true
+ options
when Hash
options = { :only_path => options[:host].nil? }.update(options.symbolize_keys)
escape = options.key?(:escape) ? options.delete(:escape) : true
- url = @controller.send(:url_for, options)
- when String
- escape = true
- url = options
+ @controller.send(:url_for, options)
+ when :back
+ escape = false
+ @controller.request.env["HTTP_REFERER"] || 'javascript:history.back()'
else
escape = false
- url = polymorphic_path(options)
+ polymorphic_path(options)
end
escape ? escape_once(url) : url
@@ -116,8 +130,8 @@ module ActionView
#
# Note that if the user has JavaScript disabled, the request will fall back
# to using GET. If <tt>:href => '#'</tt> is used and the user has JavaScript disabled
- # clicking the link will have no effect. If you are relying on the POST
- # behavior, your should check for it in your controller's action by using the
+ # clicking the link will have no effect. If you are relying on the POST
+ # behavior, your should check for it in your controller's action by using the
# request object's methods for <tt>post?</tt>, <tt>delete?</tt> or <tt>put?</tt>.
#
# You can mix and match the +html_options+ with the exception of
@@ -141,8 +155,8 @@ module ActionView
#
# link_to "Profile", :controller => "profiles", :action => "show", :id => @profile
# # => <a href="/profiles/show/1">Profile</a>
- #
- # Similarly,
+ #
+ # Similarly,
#
# link_to "Profiles", profiles_path
# # => <a href="/profiles">Profiles</a>
@@ -185,7 +199,7 @@ module ActionView
# link_to "Nonsense search", searches_path(:foo => "bar", :baz => "quux")
# # => <a href="/searches?foo=bar&amp;baz=quux">Nonsense search</a>
#
- # The three options specfic to +link_to+ (<tt>:confirm</tt>, <tt>:popup</tt>, and <tt>:method</tt>) are used as follows:
+ # The three options specific to +link_to+ (<tt>:confirm</tt>, <tt>:popup</tt>, and <tt>:method</tt>) are used as follows:
#
# link_to "Visit Other Site", "http://www.rubyonrails.org/", :confirm => "Are you sure?"
# # => <a href="http://www.rubyonrails.org/" onclick="return confirm('Are you sure?');">Visit Other Site</a>
@@ -197,9 +211,9 @@ module ActionView
# # => <a href="/images/9" onclick="window.open(this.href,'new_window_name','height=300,width=600');return false;">View Image</a>
#
# link_to "Delete Image", @image, :confirm => "Are you sure?", :method => :delete
- # # => <a href="/images/9" onclick="if (confirm('Are you sure?')) { var f = document.createElement('form');
+ # # => <a href="/images/9" onclick="if (confirm('Are you sure?')) { var f = document.createElement('form');
# f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;
- # var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method');
+ # var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method');
# m.setAttribute('value', 'delete'); f.appendChild(m);f.submit(); };return false;">Delete Image</a>
def link_to(*args, &block)
if block_given?
@@ -211,14 +225,7 @@ module ActionView
options = args.second || {}
html_options = args.third
- url = case options
- when String
- options
- when :back
- @controller.request.env["HTTP_REFERER"] || 'javascript:history.back()'
- else
- self.url_for(options)
- end
+ url = url_for(options)
if html_options
html_options = html_options.stringify_keys
@@ -228,7 +235,7 @@ module ActionView
else
tag_options = nil
end
-
+
href_attr = "href=\"#{url}\"" unless href
"<a #{href_attr}#{tag_options}>#{name || url}</a>"
end
@@ -260,7 +267,7 @@ module ActionView
# * <tt>:confirm</tt> - This will add a JavaScript confirm
# prompt with the question specified. If the user accepts, the link is
# processed normally, otherwise no action is taken.
- #
+ #
# ==== Examples
# <%= button_to "New", :action => "new" %>
# # => "<form method="post" action="/controller/new" class="button-to">
@@ -286,12 +293,12 @@ module ActionView
end
form_method = method.to_s == 'get' ? 'get' : 'post'
-
+
request_token_tag = ''
if form_method == 'post' && protect_against_forgery?
request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token)
end
-
+
if confirm = html_options.delete("confirm")
html_options["onclick"] = "return #{confirm_javascript_function(confirm)};"
end
@@ -309,7 +316,7 @@ module ActionView
# Creates a link tag of the given +name+ using a URL created by the set of
# +options+ unless the current request URI is the same as the links, in
# which case only the name is returned (or the given block is yielded, if
- # one exists). You can give link_to_unless_current a block which will
+ # one exists). You can give link_to_unless_current a block which will
# specialize the default behavior (e.g., show a "Start Here" link rather
# than the link's text).
#
@@ -336,13 +343,13 @@ module ActionView
# </ul>
#
# The implicit block given to link_to_unless_current is evaluated if the current
- # action is the action given. So, if we had a comments page and wanted to render a
+ # action is the action given. So, if we had a comments page and wanted to render a
# "Go Back" link instead of a link to the comments page, we could do something like this...
- #
- # <%=
+ #
+ # <%=
# link_to_unless_current("Comment", { :controller => 'comments', :action => 'new}) do
- # link_to("Go back", { :controller => 'posts', :action => 'index' })
- # end
+ # link_to("Go back", { :controller => 'posts', :action => 'index' })
+ # end
# %>
def link_to_unless_current(name, options = {}, html_options = {}, &block)
link_to_unless current_page?(options), name, options, html_options, &block
@@ -359,10 +366,10 @@ module ActionView
# # If the user is logged in...
# # => <a href="/controller/reply/">Reply</a>
#
- # <%=
+ # <%=
# link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) do |name|
# link_to(name, { :controller => "accounts", :action => "signup" })
- # end
+ # end
# %>
# # If the user is logged in...
# # => <a href="/controller/reply/">Reply</a>
@@ -391,10 +398,10 @@ module ActionView
# # If the user isn't logged in...
# # => <a href="/sessions/new/">Login</a>
#
- # <%=
+ # <%=
# link_to_if(@current_user.nil?, "Login", { :controller => "sessions", :action => "new" }) do
# link_to(@current_user.login, { :controller => "accounts", :action => "show", :id => @current_user })
- # end
+ # end
# %>
# # If the user isn't logged in...
# # => <a href="/sessions/new/">Login</a>
@@ -431,20 +438,20 @@ module ActionView
# * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email.
#
# ==== Examples
- # mail_to "me@domain.com"
+ # mail_to "me@domain.com"
# # => <a href="mailto:me@domain.com">me@domain.com</a>
#
- # mail_to "me@domain.com", "My email", :encode => "javascript"
- # # => <script type="text/javascript">eval(unescape('%64%6f%63...%6d%65%6e'))</script>
+ # mail_to "me@domain.com", "My email", :encode => "javascript"
+ # # => <script type="text/javascript">eval(decodeURIComponent('%64%6f%63...%27%29%3b'))</script>
#
- # mail_to "me@domain.com", "My email", :encode => "hex"
+ # mail_to "me@domain.com", "My email", :encode => "hex"
# # => <a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a>
#
- # mail_to "me@domain.com", nil, :replace_at => "_at_", :replace_dot => "_dot_", :class => "email"
+ # mail_to "me@domain.com", nil, :replace_at => "_at_", :replace_dot => "_dot_", :class => "email"
# # => <a href="mailto:me@domain.com" class="email">me_at_domain_dot_com</a>
#
# mail_to "me@domain.com", "My email", :cc => "ccaddress@domain.com",
- # :subject => "This is an example email"
+ # :subject => "This is an example email"
# # => <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a>
def mail_to(email_address, name = nil, html_options = {})
html_options = html_options.stringify_keys
@@ -469,7 +476,7 @@ module ActionView
"document.write('#{content_tag("a", name || email_address_obfuscated, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');".each_byte do |c|
string << sprintf("%%%x", c)
end
- "<script type=\"#{Mime::JS}\">eval(unescape('#{string}'))</script>"
+ "<script type=\"#{Mime::JS}\">eval(decodeURIComponent('#{string}'))</script>"
elsif encode == "hex"
email_address_encoded = ''
email_address_obfuscated.each_byte do |c|
diff --git a/actionpack/lib/action_view/locale/en-US.yml b/actionpack/lib/action_view/locale/en-US.yml
new file mode 100644
index 0000000000..818f2f93bd
--- /dev/null
+++ b/actionpack/lib/action_view/locale/en-US.yml
@@ -0,0 +1,91 @@
+"en-US":
+ number:
+ # Used in number_with_delimiter()
+ # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
+ format:
+ # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
+ separator: "."
+ # Delimets thousands (e.g. 1,000,000 is a million) (always in groups of three)
+ delimiter: ","
+ # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
+ precision: 3
+
+ # Used in number_to_currency()
+ currency:
+ format:
+ # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00)
+ format: "%u%n"
+ unit: "$"
+ # These three are to override number.format and are optional
+ separator: "."
+ delimiter: ","
+ precision: 2
+
+ # Used in number_to_percentage()
+ percentage:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # Used in number_to_precision()
+ precision:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ # precision:
+
+ # Used in number_to_human_size()
+ human:
+ format:
+ # These three are to override number.format and are optional
+ # separator:
+ delimiter: ""
+ precision: 1
+
+ # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()
+ datetime:
+ distance_in_words:
+ half_a_minute: "half a minute"
+ less_than_x_seconds:
+ one: "less than 1 second"
+ other: "less than {{count}} seconds"
+ x_seconds:
+ one: "1 second"
+ other: "{{count}} seconds"
+ less_than_x_minutes:
+ one: "less than a minute"
+ other: "less than {{count}} minutes"
+ x_minutes:
+ one: "1 minute"
+ other: "{{count}} minutes"
+ about_x_hours:
+ one: "about 1 hour"
+ other: "about {{count}} hours"
+ x_days:
+ one: "1 day"
+ other: "{{count}} days"
+ about_x_months:
+ one: "about 1 month"
+ other: "about {{count}} months"
+ x_months:
+ one: "1 month"
+ other: "{{count}} months"
+ about_x_years:
+ one: "about 1 year"
+ other: "about {{count}} years"
+ over_x_years:
+ one: "over 1 year"
+ other: "over {{count}} years"
+
+ activerecord:
+ errors:
+ template:
+ header:
+ one: "1 error prohibited this {{model}} from being saved"
+ other: "{{count}} errors prohibited this {{model}} from being saved"
+ # The variable :count is also available
+ body: "There were problems with the following fields:"
+
diff --git a/actionpack/lib/action_view/partials.rb b/actionpack/lib/action_view/partials.rb
index 5aa4c83009..443c49b870 100644
--- a/actionpack/lib/action_view/partials.rb
+++ b/actionpack/lib/action_view/partials.rb
@@ -1,14 +1,15 @@
module ActionView
- # There's also a convenience method for rendering sub templates within the current controller that depends on a single object
- # (we call this kind of sub templates for partials). It relies on the fact that partials should follow the naming convention of being
- # prefixed with an underscore -- as to separate them from regular templates that could be rendered on their own.
+ # There's also a convenience method for rendering sub templates within the current controller that depends on a
+ # single object (we call this kind of sub templates for partials). It relies on the fact that partials should
+ # follow the naming convention of being prefixed with an underscore -- as to separate them from regular
+ # templates that could be rendered on their own.
#
# In a template for Advertiser#account:
#
# <%= render :partial => "account" %>
#
- # This would render "advertiser/_account.erb" and pass the instance variable @account in as a local variable +account+ to
- # the template for display.
+ # This would render "advertiser/_account.erb" and pass the instance variable @account in as a local variable
+ # +account+ to the template for display.
#
# In another template for Advertiser#buy, we could have:
#
@@ -18,24 +19,24 @@ module ActionView
# <%= render :partial => "ad", :locals => { :ad => ad } %>
# <% end %>
#
- # This would first render "advertiser/_account.erb" with @buyer passed in as the local variable +account+, then render
- # "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display.
+ # This would first render "advertiser/_account.erb" with @buyer passed in as the local variable +account+, then
+ # render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display.
#
# == Rendering a collection of partials
#
- # The example of partial use describes a familiar pattern where a template needs to iterate over an array and render a sub
- # template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders
- # a partial by the same name as the elements contained within. So the three-lined example in "Using partials" can be rewritten
- # with a single line:
+ # The example of partial use describes a familiar pattern where a template needs to iterate over an array and
+ # render a sub template for each of the elements. This pattern has been implemented as a single method that
+ # accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
+ # example in "Using partials" can be rewritten with a single line:
#
# <%= render :partial => "ad", :collection => @advertisements %>
#
- # This will render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display. An iteration counter
- # will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the
- # example above, the template would be fed +ad_counter+.
+ # This will render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display. An
+ # iteration counter will automatically be made available to the template with a name of the form
+ # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+.
#
- # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also just keep domain objects,
- # like Active Records, in there.
+ # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
+ # just keep domain objects, like Active Records, in there.
#
# == Rendering shared partials
#
@@ -47,8 +48,9 @@ module ActionView
#
# == Rendering partials with layouts
#
- # Partials can have their own layouts applied to them. These layouts are different than the ones that are specified globally
- # for the entire action, but they work in a similar fashion. Imagine a list with two types of users:
+ # Partials can have their own layouts applied to them. These layouts are different than the ones that are
+ # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
+ # of users:
#
# <%# app/views/users/index.html.erb &>
# Here's the administrator:
@@ -68,7 +70,7 @@ module ActionView
#
# <%# app/views/users/_editor.html.erb &>
# <div id="editor">
- # Deadline: $<%= user.deadline %>
+ # Deadline: <%= user.deadline %>
# <%= yield %>
# </div>
#
@@ -82,7 +84,7 @@ module ActionView
#
# Here's the editor:
# <div id="editor">
- # Deadline: $<%= user.deadline %>
+ # Deadline: <%= user.deadline %>
# Name: <%= user.name %>
# </div>
#
@@ -101,42 +103,83 @@ module ActionView
# </div>
#
# As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
+ #
+ # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
+ # an array to layout and treat it as an enumerable.
+ #
+ # <%# app/views/users/_user.html.erb &>
+ # <div class="user">
+ # Budget: $<%= user.budget %>
+ # <%= yield user %>
+ # </div>
+ #
+ # <%# app/views/users/index.html.erb &>
+ # <% render :layout => @users do |user| %>
+ # Title: <%= user.title %>
+ # <% end %>
+ #
+ # This will render the layout for each user and yield to the block, passing the user, each time.
+ #
+ # You can also yield multiple times in one layout and use block arguments to differentiate the sections.
+ #
+ # <%# app/views/users/_user.html.erb &>
+ # <div class="user">
+ # <%= yield user, :header %>
+ # Budget: $<%= user.budget %>
+ # <%= yield user, :footer %>
+ # </div>
+ #
+ # <%# app/views/users/index.html.erb &>
+ # <% render :layout => @users do |user, section| %>
+ # <%- case section when :header -%>
+ # Title: <%= user.title %>
+ # <%- when :footer -%>
+ # Deadline: <%= user.deadline %>
+ # <%- end -%>
+ # <% end %>
module Partials
+ extend ActiveSupport::Memoizable
+
private
- def render_partial(partial_path, object_assigns = nil, local_assigns = {}) #:nodoc:
- local_assigns ||= {}
+ def render_partial(options = {}) #:nodoc:
+ local_assigns = options[:locals] || {}
- case partial_path
+ case partial_path = options[:partial]
when String, Symbol, NilClass
- pick_template(find_partial_path(partial_path)).render_partial(self, object_assigns, local_assigns)
+ if options.has_key?(:collection)
+ render_partial_collection(options)
+ else
+ _pick_partial_template(partial_path).render_partial(self, options[:object], local_assigns)
+ end
when ActionView::Helpers::FormBuilder
builder_partial_path = partial_path.class.to_s.demodulize.underscore.sub(/_builder$/, '')
- render_partial(builder_partial_path, object_assigns, (local_assigns || {}).merge(builder_partial_path.to_sym => partial_path))
+ local_assigns.merge!(builder_partial_path.to_sym => partial_path)
+ render_partial(:partial => builder_partial_path, :object => options[:object], :locals => local_assigns)
when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope
- if partial_path.any?
- collection = partial_path
- render_partial_collection(nil, collection, nil, local_assigns)
- else
- ""
- end
+ render_partial_collection(options.except(:partial).merge(:collection => partial_path))
else
- render_partial(ActionController::RecordIdentifier.partial_path(partial_path, controller.class.controller_path), partial_path, local_assigns)
+ object = partial_path
+ render_partial(
+ :partial => ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path),
+ :object => object,
+ :locals => local_assigns
+ )
end
end
- def render_partial_collection(partial_path, collection, partial_spacer_template = nil, local_assigns = {}, as = nil) #:nodoc:
- return " " if collection.empty?
+ def render_partial_collection(options = {}) #:nodoc:
+ return nil if options[:collection].blank?
- local_assigns = local_assigns ? local_assigns.clone : {}
- spacer = partial_spacer_template ? render(:partial => partial_spacer_template) : ''
- _paths = {}
- _templates = {}
+ partial = options[:partial]
+ spacer = options[:spacer_template] ? render(:partial => options[:spacer_template]) : ''
+ local_assigns = options[:locals] ? options[:locals].clone : {}
+ as = options[:as]
index = 0
- collection.map do |object|
- _partial_path ||= partial_path || ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path)
- path = _paths[_partial_path] ||= find_partial_path(_partial_path)
- template = _templates[path] ||= pick_template(path)
+ options[:collection].map do |object|
+ _partial_path ||= partial ||
+ ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path)
+ template = _pick_partial_template(_partial_path)
local_assigns[template.counter_name] = index
result = template.render_partial(self, object, local_assigns, as)
index += 1
@@ -144,14 +187,17 @@ module ActionView
end.join(spacer)
end
- def find_partial_path(partial_path)
+ def _pick_partial_template(partial_path) #:nodoc:
if partial_path.include?('/')
- "#{File.dirname(partial_path)}/_#{File.basename(partial_path)}"
- elsif respond_to?(:controller)
- "#{controller.class.controller_path}/_#{partial_path}"
+ path = File.join(File.dirname(partial_path), "_#{File.basename(partial_path)}")
+ elsif controller
+ path = "#{controller.class.controller_path}/_#{partial_path}"
else
- "_#{partial_path}"
+ path = "_#{partial_path}"
end
+
+ pick_template(path)
end
+ memoize :_pick_partial_template
end
end
diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb
index b0ab7d0c67..d6bf2137af 100644
--- a/actionpack/lib/action_view/paths.rb
+++ b/actionpack/lib/action_view/paths.rb
@@ -3,7 +3,7 @@ module ActionView #:nodoc:
def self.type_cast(obj)
if obj.is_a?(String)
if Base.warn_cache_misses && defined?(Rails) && Rails.initialized?
- Rails.logger.debug "[PERFORMANCE] Processing view path during a " +
+ Base.logger.debug "[PERFORMANCE] Processing view path during a " +
"request. This an expense disk operation that should be done at " +
"boot. You can manually process this view path with " +
"ActionView::Base.process_view_paths(#{obj.inspect}) and set it " +
@@ -15,70 +15,104 @@ module ActionView #:nodoc:
end
end
+ def initialize(*args)
+ super(*args).map! { |obj| self.class.type_cast(obj) }
+ end
+
+ def <<(obj)
+ super(self.class.type_cast(obj))
+ end
+
+ def concat(array)
+ super(array.map! { |obj| self.class.type_cast(obj) })
+ end
+
+ def insert(index, obj)
+ super(index, self.class.type_cast(obj))
+ end
+
+ def push(*objs)
+ super(*objs.map { |obj| self.class.type_cast(obj) })
+ end
+
+ def unshift(*objs)
+ super(*objs.map { |obj| self.class.type_cast(obj) })
+ end
+
class Path #:nodoc:
+ def self.eager_load_templates!
+ @eager_load_templates = true
+ end
+
+ def self.eager_load_templates?
+ @eager_load_templates || false
+ end
+
attr_reader :path, :paths
- delegate :to_s, :to_str, :inspect, :to => :path
+ delegate :to_s, :to_str, :hash, :inspect, :to => :path
- def initialize(path)
+ def initialize(path, load = true)
+ raise ArgumentError, "path already is a Path class" if path.is_a?(Path)
@path = path.freeze
- reload!
+ reload! if load
end
def ==(path)
to_str == path.to_str
end
+ def eql?(path)
+ to_str == path.to_str
+ end
+
def [](path)
+ raise "Unloaded view path! #{@path}" unless @loaded
@paths[path]
end
+ def loaded?
+ @loaded ? true : false
+ end
+
+ def load
+ reload! unless loaded?
+ self
+ end
+
# Rebuild load path directory cache
def reload!
@paths = {}
templates_in_path do |template|
+ # Eager load memoized methods and freeze cached template
+ template.freeze if self.class.eager_load_templates?
+
@paths[template.path] = template
@paths[template.path_without_extension] ||= template
end
@paths.freeze
+ @loaded = true
end
private
def templates_in_path
(Dir.glob("#{@path}/**/*/**") | Dir.glob("#{@path}/**")).each do |file|
unless File.directory?(file)
- template = Template.new(file.split("#{self}/").last, self)
- # Eager load memoized methods and freeze cached template
- template.freeze if Base.cache_template_loading
- yield template
+ yield Template.new(file.split("#{self}/").last, self)
end
end
end
end
- def initialize(*args)
- super(*args).map! { |obj| self.class.type_cast(obj) }
+ def load
+ each { |path| path.load }
end
def reload!
each { |path| path.reload! }
end
- def <<(obj)
- super(self.class.type_cast(obj))
- end
-
- def push(*objs)
- delete_paths!(objs)
- super(*objs.map { |obj| self.class.type_cast(obj) })
- end
-
- def unshift(*objs)
- delete_paths!(objs)
- super(*objs.map { |obj| self.class.type_cast(obj) })
- end
-
def [](template_path)
each do |path|
if template = path[template_path]
@@ -87,10 +121,5 @@ module ActionView #:nodoc:
end
nil
end
-
- private
- def delete_paths!(paths)
- paths.each { |p1| delete_if { |p2| p1.to_s == p2.to_s } }
- end
end
end
diff --git a/actionpack/lib/action_view/renderable.rb b/actionpack/lib/action_view/renderable.rb
index f66356c939..c011f21550 100644
--- a/actionpack/lib/action_view/renderable.rb
+++ b/actionpack/lib/action_view/renderable.rb
@@ -3,11 +3,15 @@ module ActionView
# NOTE: The template that this mixin is beening include into is frozen
# So you can not set or modify any instance variables
+ extend ActiveSupport::Memoizable
+
def self.included(base)
@@mutex = Mutex.new
end
- include ActiveSupport::Memoizable
+ def filename
+ 'compiled-template'
+ end
def handler
Template.handler_class_for_extension(extension)
@@ -15,19 +19,29 @@ module ActionView
memoize :handler
def compiled_source
- handler.new(nil).compile(self) if handler.compilable?
+ handler.call(self)
end
memoize :compiled_source
def render(view, local_assigns = {})
+ compile(local_assigns)
+
view._first_render ||= self
view._last_render = self
+
view.send(:evaluate_assigns)
- compile(local_assigns) if handler.compilable?
- handler.new(view).render(self, 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")
+ view.capture(*names, &proc)
+ else
+ view.instance_variable_get("@content_for_#{names.first || 'layout'}")
+ end
+ end
end
- def method(local_assigns)
+ def method_name(local_assigns)
if local_assigns && local_assigns.any?
local_assigns_keys = "locals_#{local_assigns.keys.map { |k| k.to_s }.sort.join('_')}"
end
@@ -35,35 +49,41 @@ module ActionView
end
private
- # Compile and evaluate the template's code
+ # Compile and evaluate the template's code (if necessary)
def compile(local_assigns)
- render_symbol = method(local_assigns)
+ render_symbol = method_name(local_assigns)
@@mutex.synchronize do
- return false unless recompile?(render_symbol)
-
- locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join
-
- source = <<-end_src
- def #{render_symbol}(local_assigns)
- old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
- ensure
- self.output_buffer = old_output_buffer
- end
- end_src
-
- begin
- file_name = respond_to?(:filename) ? filename : 'compiled-template'
- ActionView::Base::CompiledTemplates.module_eval(source, file_name, 0)
- rescue Exception => e # errors from template code
- if logger = ActionController::Base.logger
- logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
- logger.debug "Function body: #{source}"
- logger.debug "Backtrace: #{e.backtrace.join("\n")}"
- end
-
- raise ActionView::TemplateError.new(self, {}, e)
+ if recompile?(render_symbol)
+ compile!(render_symbol, local_assigns)
+ end
+ end
+ end
+
+ def compile!(render_symbol, local_assigns)
+ locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join
+
+ source = <<-end_src
+ def #{render_symbol}(local_assigns)
+ old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
+ ensure
+ self.output_buffer = old_output_buffer
end
+ end_src
+
+ begin
+ logger = 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
+ logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
+ logger.debug "Function body: #{source}"
+ logger.debug "Backtrace: #{e.backtrace.join("\n")}"
+ end
+
+ raise ActionView::TemplateError.new(self, {}, e)
end
end
@@ -71,8 +91,7 @@ module ActionView
# The template will be compiled if the file has not been compiled yet, or
# if local_assigns has a new key, which isn't supported by the compiled code yet.
def recompile?(symbol)
- meth = Base::CompiledTemplates.instance_method(template.method) rescue nil
- !(meth && Base.cache_template_loading)
+ !(ActionView::PathSet::Path.eager_load_templates? && Base::CompiledTemplates.method_defined?(symbol))
end
end
end
diff --git a/actionpack/lib/action_view/renderable_partial.rb b/actionpack/lib/action_view/renderable_partial.rb
index fdb1a5e6a7..342850f0f0 100644
--- a/actionpack/lib/action_view/renderable_partial.rb
+++ b/actionpack/lib/action_view/renderable_partial.rb
@@ -3,7 +3,7 @@ module ActionView
# NOTE: The template that this mixin is beening include into is frozen
# So you can not set or modify any instance variables
- include ActiveSupport::Memoizable
+ extend ActiveSupport::Memoizable
def variable_name
name.sub(/\A_/, '').to_sym
diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb
index 304aec3a4c..5dc6708431 100644
--- a/actionpack/lib/action_view/template.rb
+++ b/actionpack/lib/action_view/template.rb
@@ -1,7 +1,7 @@
module ActionView #:nodoc:
class Template
extend TemplateHandlers
- include ActiveSupport::Memoizable
+ extend ActiveSupport::Memoizable
include Renderable
attr_accessor :filename, :load_path, :base_path, :name, :format, :extension
@@ -22,6 +22,19 @@ module ActionView #:nodoc:
end
memoize :format_and_extension
+ def multipart?
+ format && format.include?('.')
+ end
+
+ def content_type
+ format.gsub('.', '/')
+ end
+
+ def mime_type
+ Mime::Type.lookup_by_extension(format) if format
+ end
+ memoize :mime_type
+
def path
[base_path, [name, format, extension].compact.join('.')].compact.join('/')
end
@@ -70,7 +83,7 @@ module ActionView #:nodoc:
load_paths = Array(load_paths) + [nil]
load_paths.each do |load_path|
file = [load_path, path].compact.join('/')
- return load_path, file if File.exist?(file)
+ return load_path, file if File.file?(file)
end
raise MissingTemplate.new(load_paths, path)
end
@@ -79,7 +92,7 @@ module ActionView #:nodoc:
# [base_path, name, format, extension]
def split(file)
if m = file.match(/^(.*\/)?([^\.]+)\.?(\w+)?\.?(\w+)?\.?(\w+)?$/)
- if m[5] # Mulipart formats
+ if m[5] # Multipart formats
[m[1], m[2], "#{m[3]}.#{m[4]}", m[5]]
elsif m[4] # Single format
[m[1], m[2], m[3], m[4]]
diff --git a/actionpack/lib/action_view/template_error.rb b/actionpack/lib/action_view/template_error.rb
index 35fc07bdb2..2368662f31 100644
--- a/actionpack/lib/action_view/template_error.rb
+++ b/actionpack/lib/action_view/template_error.rb
@@ -7,7 +7,7 @@ module ActionView
attr_reader :original_exception
def initialize(template, assigns, original_exception)
- @base_path = template.base_path
+ @base_path = template.base_path.to_s
@assigns, @source, @original_exception = assigns.dup, template.source, original_exception
@file_path = template.filename
@backtrace = compute_backtrace
diff --git a/actionpack/lib/action_view/template_handler.rb b/actionpack/lib/action_view/template_handler.rb
index 1afea21f67..d7e7c9b199 100644
--- a/actionpack/lib/action_view/template_handler.rb
+++ b/actionpack/lib/action_view/template_handler.rb
@@ -1,25 +1,14 @@
-module ActionView
- class TemplateHandler
- def self.compilable?
- false
- end
-
- def initialize(view)
- @view = view
- end
-
- def render(template, local_assigns = {})
- end
-
- def compile(template)
- end
+# Legacy TemplateHandler stub
- def compilable?
- self.class.compilable?
+module ActionView
+ module TemplateHandlers
+ module Compilable
end
+ end
- # Called by CacheHelper#cache
- def cache_fragment(block, name = {}, options = nil)
+ class TemplateHandler
+ def self.call(template)
+ new.compile(template)
end
end
end
diff --git a/actionpack/lib/action_view/template_handlers.rb b/actionpack/lib/action_view/template_handlers.rb
index 1471e99e01..6c8aa4c2a7 100644
--- a/actionpack/lib/action_view/template_handlers.rb
+++ b/actionpack/lib/action_view/template_handlers.rb
@@ -1,5 +1,4 @@
require 'action_view/template_handler'
-require 'action_view/template_handlers/compilable'
require 'action_view/template_handlers/builder'
require 'action_view/template_handlers/erb'
require 'action_view/template_handlers/rjs'
diff --git a/actionpack/lib/action_view/template_handlers/builder.rb b/actionpack/lib/action_view/template_handlers/builder.rb
index cbe53e11d8..7d24a5c423 100644
--- a/actionpack/lib/action_view/template_handlers/builder.rb
+++ b/actionpack/lib/action_view/template_handlers/builder.rb
@@ -6,18 +6,12 @@ module ActionView
include Compilable
def compile(template)
- # ActionMailer does not have a response
- "controller.respond_to?(:response) && controller.response.content_type ||= Mime::XML;" +
+ "set_controller_content_type(Mime::XML);" +
"xml = ::Builder::XmlMarkup.new(:indent => 2);" +
+ "self.output_buffer = xml.target!;" +
template.source +
";xml.target!;"
end
-
- def cache_fragment(block, name = {}, options = nil)
- @view.fragment_for(block, name, options) do
- eval('xml.target!', block.binding)
- end
- end
end
end
end
diff --git a/actionpack/lib/action_view/template_handlers/compilable.rb b/actionpack/lib/action_view/template_handlers/compilable.rb
deleted file mode 100644
index a0ebaefeef..0000000000
--- a/actionpack/lib/action_view/template_handlers/compilable.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-module ActionView
- module TemplateHandlers
- module Compilable
- def self.included(base)
- base.extend ClassMethod
- end
-
- module ClassMethod
- # If a handler is mixin this module, set compilable to true
- def compilable?
- true
- end
- end
-
- def render(template, local_assigns = {})
- @view.send(:execute, template, local_assigns)
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/template_handlers/erb.rb b/actionpack/lib/action_view/template_handlers/erb.rb
index ac715e30c2..3def949f1e 100644
--- a/actionpack/lib/action_view/template_handlers/erb.rb
+++ b/actionpack/lib/action_view/template_handlers/erb.rb
@@ -48,14 +48,11 @@ module ActionView
self.erb_trim_mode = '-'
def compile(template)
- src = ::ERB.new(template.source, nil, erb_trim_mode, '@output_buffer').src
- "__in_erb_template=true;#{src}"
- end
+ src = ::ERB.new("<% __in_erb_template=true %>#{template.source}", nil, erb_trim_mode, '@output_buffer').src
- def cache_fragment(block, name = {}, options = nil) #:nodoc:
- @view.fragment_for(block, name, options) do
- @view.response.template.output_buffer
- end
+ # Ruby 1.9 prepends an encoding to the source. However this is
+ # useless because you can only set an encoding on the first line
+ RUBY_VERSION >= '1.9' ? src.sub(/\A#coding:.*\n/, '') : src
end
end
end
diff --git a/actionpack/lib/action_view/template_handlers/rjs.rb b/actionpack/lib/action_view/template_handlers/rjs.rb
index 3892bf1d6e..a700655c9a 100644
--- a/actionpack/lib/action_view/template_handlers/rjs.rb
+++ b/actionpack/lib/action_view/template_handlers/rjs.rb
@@ -7,17 +7,6 @@ module ActionView
"controller.response.content_type ||= Mime::JS;" +
"update_page do |page|;#{template.source}\nend"
end
-
- def cache_fragment(block, name = {}, options = nil) #:nodoc:
- @view.fragment_for(block, name, options) do
- begin
- debug_mode, ActionView::Base.debug_rjs = ActionView::Base.debug_rjs, false
- eval('page.to_s', block.binding)
- ensure
- ActionView::Base.debug_rjs = debug_mode
- end
- end
- end
end
end
end
diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb
index 1a3c93c283..adbb37fd09 100644
--- a/actionpack/lib/action_view/test_case.rb
+++ b/actionpack/lib/action_view/test_case.rb
@@ -25,9 +25,7 @@ module ActionView
end
end
- ActionView::Base.helper_modules.each do |helper_module|
- include helper_module
- end
+ include ActionView::Helpers
include ActionController::PolymorphicRoutes
include ActionController::RecordIdentifier
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 0d2e0f273a..9db4cddd6a 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -22,8 +22,8 @@ ActiveSupport::Deprecation.debug = true
ActionController::Base.logger = nil
ActionController::Routing::Routes.reload rescue nil
-ActionView::Base.cache_template_loading = true
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
+ActionView::PathSet::Path.eager_load_templates!
ActionController::Base.view_paths = FIXTURE_LOAD_PATH
# Wrap tests that use Mocha and skip if unavailable.
diff --git a/actionpack/test/activerecord/render_partial_with_record_identification_test.rb b/actionpack/test/activerecord/render_partial_with_record_identification_test.rb
index a82a1a3023..d75cb2b53a 100644
--- a/actionpack/test/activerecord/render_partial_with_record_identification_test.rb
+++ b/actionpack/test/activerecord/render_partial_with_record_identification_test.rb
@@ -39,6 +39,11 @@ class RenderPartialWithRecordIdentificationController < ActionController::Base
@developers = Developer.find(:all)
render :partial => @developers
end
+
+ def render_with_record_collection_and_spacer_template
+ @developer = Developer.find(1)
+ render :partial => @developer.projects, :spacer_template => 'test/partial_only'
+ end
end
class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase
@@ -81,6 +86,11 @@ class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase
assert_equal 'DavidJamisfixture_3fixture_4fixture_5fixture_6fixture_7fixture_8fixture_9fixture_10Jamis', @response.body
end
+ def test_render_with_record_collection_and_spacer_template
+ get :render_with_record_collection_and_spacer_template
+ assert_equal 'Active Recordonly partialActive Controller', @response.body
+ end
+
def test_rendering_partial_with_has_one_association
mascot = Company.find(1).mascot
get :render_with_has_one_association
diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb
index 5af579f3e3..1531e7c21a 100644
--- a/actionpack/test/controller/assert_select_test.rb
+++ b/actionpack/test/controller/assert_select_test.rb
@@ -17,6 +17,8 @@ unless defined?(ActionMailer)
end
end
+ActionMailer::Base.template_root = FIXTURE_LOAD_PATH
+
class AssertSelectTest < Test::Unit::TestCase
class AssertSelectController < ActionController::Base
def response_with=(content)
@@ -69,11 +71,10 @@ class AssertSelectTest < Test::Unit::TestCase
ActionMailer::Base.deliveries = []
end
-
def teardown
ActionMailer::Base.deliveries.clear
end
-
+
def assert_failure(message, &block)
e = assert_raises(AssertionFailedError, &block)
assert_match(message, e.message) if Regexp === message
@@ -91,7 +92,6 @@ class AssertSelectTest < Test::Unit::TestCase
assert_failure(/Expected at least 1 element matching \"p\", found 0/) { assert_select "p" }
end
-
def test_equality_true_false
render_html %Q{<div id="1"></div><div id="2"></div>}
assert_nothing_raised { assert_select "div" }
@@ -102,7 +102,6 @@ class AssertSelectTest < Test::Unit::TestCase
assert_nothing_raised { assert_select "p", false }
end
-
def test_equality_string_and_regexp
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
assert_nothing_raised { assert_select "div", "foo" }
@@ -116,7 +115,6 @@ class AssertSelectTest < Test::Unit::TestCase
assert_raises(AssertionFailedError) { assert_select "p", :text=>/foobar/ }
end
-
def test_equality_of_html
render_html %Q{<p>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</p>}
text = "\"This is not a big problem,\" he said."
@@ -135,7 +133,6 @@ class AssertSelectTest < Test::Unit::TestCase
assert_raises(AssertionFailedError) { assert_select "pre", :html=>text }
end
-
def test_counts
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
assert_nothing_raised { assert_select "div", 2 }
@@ -166,7 +163,6 @@ class AssertSelectTest < Test::Unit::TestCase
end
end
-
def test_substitution_values
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
assert_select "div#?", /\d+/ do |elements|
@@ -181,7 +177,6 @@ class AssertSelectTest < Test::Unit::TestCase
end
end
-
def test_nested_assert_select
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
assert_select "div" do |elements|
@@ -200,7 +195,7 @@ class AssertSelectTest < Test::Unit::TestCase
assert_select "#3", false
end
end
-
+
assert_failure(/Expected at least 1 element matching \"#4\", found 0\./) do
assert_select "div" do
assert_select "#4"
@@ -208,7 +203,6 @@ class AssertSelectTest < Test::Unit::TestCase
end
end
-
def test_assert_select_text_match
render_html %Q{<div id="1"><span>foo</span></div><div id="2"><span>bar</span></div>}
assert_select "div" do
@@ -225,7 +219,6 @@ class AssertSelectTest < Test::Unit::TestCase
end
end
-
# With single result.
def test_assert_select_from_rjs_with_single_result
render_rjs do |page|
@@ -255,19 +248,16 @@ class AssertSelectTest < Test::Unit::TestCase
end
end
-
#
# Test css_select.
#
-
def test_css_select
render_html %Q{<div id="1"></div><div id="2"></div>}
assert 2, css_select("div").size
assert 0, css_select("p").size
end
-
def test_nested_css_select
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
assert_select "div#?", /\d+/ do |elements|
@@ -286,7 +276,6 @@ class AssertSelectTest < Test::Unit::TestCase
end
end
-
# With one result.
def test_css_select_from_rjs_with_single_result
render_rjs do |page|
@@ -309,12 +298,10 @@ class AssertSelectTest < Test::Unit::TestCase
assert_equal 1, css_select("#2").size
end
-
#
# Test assert_select_rjs.
#
-
# Test that we can pick up all statements in the result.
def test_assert_select_rjs_picks_up_all_statements
render_rjs do |page|
@@ -381,7 +368,6 @@ class AssertSelectTest < Test::Unit::TestCase
assert_raises(AssertionFailedError) { assert_select_rjs "test4" }
end
-
def test_assert_select_rjs_for_replace
render_rjs do |page|
page.replace "test1", "<div id=\"1\">foo</div>"
@@ -479,7 +465,7 @@ class AssertSelectTest < Test::Unit::TestCase
end
end
end
-
+
# Simple hide
def test_assert_select_rjs_for_hide
render_rjs do |page|
@@ -500,7 +486,7 @@ class AssertSelectTest < Test::Unit::TestCase
end
end
end
-
+
# Simple toggle
def test_assert_select_rjs_for_toggle
render_rjs do |page|
@@ -521,7 +507,7 @@ class AssertSelectTest < Test::Unit::TestCase
end
end
end
-
+
# Non-positioned insert.
def test_assert_select_rjs_for_nonpositioned_insert
render_rjs do |page|
@@ -568,7 +554,7 @@ class AssertSelectTest < Test::Unit::TestCase
assert_select "div", 4
end
end
-
+
# Simple selection from a single result.
def test_nested_assert_select_rjs_with_single_result
render_rjs do |page|
@@ -600,7 +586,6 @@ class AssertSelectTest < Test::Unit::TestCase
end
end
-
def test_feed_item_encoded
render_xml <<-EOF
<rss version="2.0">
@@ -654,7 +639,6 @@ EOF
end
end
-
#
# Test assert_select_email
#
@@ -670,7 +654,6 @@ EOF
end
end
-
protected
def render_html(html)
@controller.response_with = html
diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb
index 34c0200fe8..d49cc2a9aa 100644
--- a/actionpack/test/controller/base_test.rb
+++ b/actionpack/test/controller/base_test.rb
@@ -7,6 +7,7 @@ module Submodule
end
class ContainedNonEmptyController < ActionController::Base
def public_action
+ render :nothing => true
end
hide_action :hidden_action
@@ -105,6 +106,18 @@ end
class PerformActionTest < Test::Unit::TestCase
+ class MockLogger
+ attr_reader :logged
+
+ def initialize
+ @logged = []
+ end
+
+ def method_missing(method, *args)
+ @logged << args.first
+ end
+ end
+
def use_controller(controller_class)
@controller = controller_class.new
@@ -142,6 +155,13 @@ class PerformActionTest < Test::Unit::TestCase
get :another_hidden_action
assert_response 404
end
+
+ def test_namespaced_action_should_log_module_name
+ use_controller Submodule::ContainedNonEmptyController
+ @controller.logger = MockLogger.new
+ get :public_action
+ assert_match /Processing\sSubmodule::ContainedNonEmptyController#public_action/, @controller.logger.logged[1]
+ end
end
class DefaultUrlOptionsTest < Test::Unit::TestCase
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
index c30f7be700..b6cdd116e5 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -109,7 +109,7 @@ class PageCachingTest < Test::Unit::TestCase
uses_mocha("should_cache_ok_at_custom_path") do
def test_should_cache_ok_at_custom_path
- @request.expects(:path).returns("/index.html")
+ @request.stubs(:path).returns("/index.html")
get :ok
assert_response :ok
assert File.exist?("#{FILE_STORE_PATH}/index.html")
@@ -148,7 +148,6 @@ class PageCachingTest < Test::Unit::TestCase
end
end
-
class ActionCachingTestController < ActionController::Base
caches_action :index, :redirected, :forbidden, :if => Proc.new { |c| !c.request.format.json? }, :expires_in => 1.hour
caches_action :show, :cache_path => 'http://test.host/custom/show'
@@ -489,54 +488,54 @@ class FragmentCachingTest < Test::Unit::TestCase
def test_fragment_cache_key
assert_equal 'views/what a key', @controller.fragment_cache_key('what a key')
- assert_equal( "views/test.host/fragment_caching_test/some_action",
- @controller.fragment_cache_key(:controller => 'fragment_caching_test',:action => 'some_action'))
+ assert_equal "views/test.host/fragment_caching_test/some_action",
+ @controller.fragment_cache_key(:controller => 'fragment_caching_test',:action => 'some_action')
end
- def test_read_fragment__with_caching_enabled
+ def test_read_fragment_with_caching_enabled
@store.write('views/name', 'value')
assert_equal 'value', @controller.read_fragment('name')
end
- def test_read_fragment__with_caching_disabled
+ def test_read_fragment_with_caching_disabled
ActionController::Base.perform_caching = false
@store.write('views/name', 'value')
assert_nil @controller.read_fragment('name')
end
- def test_fragment_exist__with_caching_enabled
+ def test_fragment_exist_with_caching_enabled
@store.write('views/name', 'value')
assert @controller.fragment_exist?('name')
assert !@controller.fragment_exist?('other_name')
end
- def test_fragment_exist__with_caching_disabled
+ def test_fragment_exist_with_caching_disabled
ActionController::Base.perform_caching = false
@store.write('views/name', 'value')
assert !@controller.fragment_exist?('name')
assert !@controller.fragment_exist?('other_name')
end
- def test_write_fragment__with_caching_enabled
+ def test_write_fragment_with_caching_enabled
assert_nil @store.read('views/name')
assert_equal 'value', @controller.write_fragment('name', 'value')
assert_equal 'value', @store.read('views/name')
end
- def test_write_fragment__with_caching_disabled
+ def test_write_fragment_with_caching_disabled
assert_nil @store.read('views/name')
ActionController::Base.perform_caching = false
assert_equal nil, @controller.write_fragment('name', 'value')
assert_nil @store.read('views/name')
end
- def test_expire_fragment__with_simple_key
+ def test_expire_fragment_with_simple_key
@store.write('views/name', 'value')
@controller.expire_fragment 'name'
assert_nil @store.read('views/name')
end
- def test_expire_fragment__with__regexp
+ def test_expire_fragment_with_regexp
@store.write('views/name', 'value')
@store.write('views/another_name', 'another_value')
@store.write('views/primalgrasp', 'will not expire ;-)')
@@ -548,14 +547,14 @@ class FragmentCachingTest < Test::Unit::TestCase
assert_equal 'will not expire ;-)', @store.read('views/primalgrasp')
end
- def test_fragment_for__with_disabled_caching
+ def test_fragment_for_with_disabled_caching
ActionController::Base.perform_caching = false
@store.write('views/expensive', 'fragment content')
fragment_computed = false
buffer = 'generated till now -> '
- @controller.fragment_for(Proc.new { fragment_computed = true }, 'expensive') { buffer }
+ @controller.fragment_for(buffer, 'expensive') { fragment_computed = true }
assert fragment_computed
assert_equal 'generated till now -> ', buffer
@@ -566,53 +565,13 @@ class FragmentCachingTest < Test::Unit::TestCase
fragment_computed = false
buffer = 'generated till now -> '
- @controller.fragment_for(Proc.new { fragment_computed = true }, 'expensive') { buffer}
+ @controller.fragment_for(buffer, 'expensive') { fragment_computed = true }
assert !fragment_computed
assert_equal 'generated till now -> fragment content', buffer
end
-
- def test_cache_erb_fragment
- @store.write('views/expensive', 'fragment content')
- @controller.response.template.output_buffer = 'generated till now -> '
-
- assert_equal( 'generated till now -> fragment content',
- ActionView::TemplateHandlers::ERB.new(@controller).cache_fragment(Proc.new{ }, 'expensive'))
- end
-
- def test_cache_rxml_fragment
- @store.write('views/expensive', 'fragment content')
- xml = 'generated till now -> '
- class << xml; def target!; to_s; end; end
-
- assert_equal( 'generated till now -> fragment content',
- ActionView::TemplateHandlers::Builder.new(@controller).cache_fragment(Proc.new{ }, 'expensive'))
- end
-
- def test_cache_rjs_fragment
- @store.write('views/expensive', 'fragment content')
- page = 'generated till now -> '
-
- assert_equal( 'generated till now -> fragment content',
- ActionView::TemplateHandlers::RJS.new(@controller).cache_fragment(Proc.new{ }, 'expensive'))
- end
-
- def test_cache_rjs_fragment_debug_mode_does_not_interfere
- @store.write('views/expensive', 'fragment content')
- page = 'generated till now -> '
-
- begin
- debug_mode, ActionView::Base.debug_rjs = ActionView::Base.debug_rjs, true
- assert_equal( 'generated till now -> fragment content',
- ActionView::TemplateHandlers::RJS.new(@controller).cache_fragment(Proc.new{ }, 'expensive'))
- assert ActionView::Base.debug_rjs
- ensure
- ActionView::Base.debug_rjs = debug_mode
- end
- end
end
-
class FunctionalCachingController < ActionController::Base
def fragment_cached
end
@@ -629,6 +588,13 @@ class FunctionalCachingController < ActionController::Base
end
end
+ def formatted_fragment_cached
+ respond_to do |format|
+ format.html
+ format.xml
+ format.js
+ end
+ end
def rescue_action(e)
raise e
@@ -678,4 +644,35 @@ CACHED
assert_match /Fragment caching in a partial/, @response.body
assert_match "Fragment caching in a partial", @store.read('views/test.host/functional_caching/js_fragment_cached_with_partial')
end
+
+ def test_html_formatted_fragment_caching
+ get :formatted_fragment_cached, :format => "html"
+ assert_response :success
+ expected_body = "<body>\n<p>ERB</p>\n</body>"
+
+ assert_equal expected_body, @response.body
+
+ assert_equal "<p>ERB</p>", @store.read('views/test.host/functional_caching/formatted_fragment_cached')
+ end
+
+ def test_xml_formatted_fragment_caching
+ get :formatted_fragment_cached, :format => "xml"
+ assert_response :success
+ expected_body = "<body>\n <p>Builder</p>\n</body>\n"
+
+ assert_equal expected_body, @response.body
+
+ assert_equal " <p>Builder</p>\n", @store.read('views/test.host/functional_caching/formatted_fragment_cached')
+ end
+
+ def test_js_formatted_fragment_caching
+ get :formatted_fragment_cached, :format => "js"
+ assert_response :success
+ expected_body = %(title = "Hey";\n$("element_1").visualEffect("highlight");\n) +
+ %($("element_2").visualEffect("highlight");\nfooter = "Bye";)
+ assert_equal expected_body, @response.body
+
+ assert_equal ['$("element_1").visualEffect("highlight");', '$("element_2").visualEffect("highlight");'],
+ @store.read('views/test.host/functional_caching/formatted_fragment_cached')
+ end
end
diff --git a/actionpack/test/controller/cgi_test.rb b/actionpack/test/controller/cgi_test.rb
index bf3b8b788e..813171857a 100755..100644
--- a/actionpack/test/controller/cgi_test.rb
+++ b/actionpack/test/controller/cgi_test.rb
@@ -53,6 +53,15 @@ class BaseCgiTest < Test::Unit::TestCase
end
def default_test; end
+
+ private
+
+ def set_content_data(data)
+ @request.env['REQUEST_METHOD'] = 'POST'
+ @request.env['CONTENT_LENGTH'] = data.length
+ @request.env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=utf-8'
+ @request.env['RAW_POST_DATA'] = data
+ end
end
class CgiRequestTest < BaseCgiTest
@@ -66,7 +75,7 @@ class CgiRequestTest < BaseCgiTest
assert_equal "rubyonrails.org:8080", @request.host_with_port
@request_hash['HTTP_X_FORWARDED_HOST'] = "www.firsthost.org, www.secondhost.org"
- assert_equal "www.secondhost.org", @request.host
+ assert_equal "www.secondhost.org", @request.host(true)
end
def test_http_host_with_default_port_overrides_server_port
@@ -155,10 +164,8 @@ end
class CgiRequestParamsParsingTest < BaseCgiTest
def test_doesnt_break_when_content_type_has_charset
- data = 'flamenco=love'
- @request.env['CONTENT_LENGTH'] = data.length
- @request.env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=utf-8'
- @request.env['RAW_POST_DATA'] = data
+ set_content_data 'flamenco=love'
+
assert_equal({"flamenco"=> "love"}, @request.request_parameters)
end
@@ -168,6 +175,41 @@ class CgiRequestParamsParsingTest < BaseCgiTest
end
end
+class CgiRequestContentTypeTest < BaseCgiTest
+ def test_html_content_type_verification
+ @request.env['CONTENT_TYPE'] = Mime::HTML.to_s
+ assert @request.content_type.verify_request?
+ end
+
+ def test_xml_content_type_verification
+ @request.env['CONTENT_TYPE'] = Mime::XML.to_s
+ assert !@request.content_type.verify_request?
+ end
+end
+
+class CgiRequestMethodTest < BaseCgiTest
+ def test_get
+ assert_equal :get, @request.request_method
+ end
+
+ def test_post
+ @request.env['REQUEST_METHOD'] = 'POST'
+ assert_equal :post, @request.request_method
+ end
+
+ def test_put
+ set_content_data '_method=put'
+
+ assert_equal :put, @request.request_method
+ end
+
+ def test_delete
+ set_content_data '_method=delete'
+
+ assert_equal :delete, @request.request_method
+ end
+end
+
class CgiRequestNeedsRewoundTest < BaseCgiTest
def test_body_should_be_rewound
data = 'foo'
diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb
index d457d13aef..ae71d62e11 100644
--- a/actionpack/test/controller/content_type_test.rb
+++ b/actionpack/test/controller/content_type_test.rb
@@ -19,6 +19,11 @@ class ContentTypeController < ActionController::Base
render :text => "hello world!"
end
+ def render_nil_charset_from_body
+ response.charset = nil
+ render :text => "hello world!"
+ end
+
def render_default_for_rhtml
end
@@ -85,8 +90,23 @@ class ContentTypeTest < Test::Unit::TestCase
def test_charset_from_body
get :render_charset_from_body
+ assert_equal Mime::HTML, @response.content_type
assert_equal "utf-16", @response.charset
+ end
+
+ def test_nil_charset_from_body
+ get :render_nil_charset_from_body
assert_equal Mime::HTML, @response.content_type
+ assert_equal "utf-8", @response.charset, @response.headers.inspect
+ end
+
+ def test_nil_default_for_rhtml
+ ContentTypeController.default_charset = nil
+ get :render_default_for_rhtml
+ assert_equal Mime::HTML, @response.content_type
+ assert_nil @response.charset, @response.headers.inspect
+ ensure
+ ContentTypeController.default_charset = "utf-8"
end
def test_default_for_rhtml
@@ -128,23 +148,23 @@ class AcceptBasedContentTypeTest < ActionController::TestCase
def test_render_default_content_types_for_respond_to
- @request.env["HTTP_ACCEPT"] = Mime::HTML.to_s
+ @request.accept = Mime::HTML.to_s
get :render_default_content_types_for_respond_to
assert_equal Mime::HTML, @response.content_type
- @request.env["HTTP_ACCEPT"] = Mime::JS.to_s
+ @request.accept = Mime::JS.to_s
get :render_default_content_types_for_respond_to
assert_equal Mime::JS, @response.content_type
end
def test_render_default_content_types_for_respond_to_with_template
- @request.env["HTTP_ACCEPT"] = Mime::XML.to_s
+ @request.accept = Mime::XML.to_s
get :render_default_content_types_for_respond_to
assert_equal Mime::XML, @response.content_type
end
def test_render_default_content_types_for_respond_to_with_overwrite
- @request.env["HTTP_ACCEPT"] = Mime::RSS.to_s
+ @request.accept = Mime::RSS.to_s
get :render_default_content_types_for_respond_to
assert_equal Mime::XML, @response.content_type
end
diff --git a/actionpack/test/controller/cookie_test.rb b/actionpack/test/controller/cookie_test.rb
index b45fbb17d3..5a6fb49861 100644
--- a/actionpack/test/controller/cookie_test.rb
+++ b/actionpack/test/controller/cookie_test.rb
@@ -60,7 +60,7 @@ class CookieTest < Test::Unit::TestCase
end
def test_setting_cookie_for_fourteen_days_with_symbols
- get :authenticate_for_fourteen_days
+ get :authenticate_for_fourteen_days_with_symbols
assert_equal [ CGI::Cookie::new("name" => "user_name", "value" => "david", "expires" => Time.local(2005, 10, 10)) ], @response.headers["cookie"]
end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index 475e13897b..c986941140 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -253,7 +253,14 @@ class IntegrationProcessTest < ActionController::IntegrationTest
session :off
def get
- render :text => "OK", :status => 200
+ respond_to do |format|
+ format.html { render :text => "OK", :status => 200 }
+ format.js { render :text => "JS OK", :status => 200 }
+ end
+ end
+
+ def get_with_params
+ render :text => "foo: #{params[:foo]}", :status => 200
end
def post
@@ -265,6 +272,10 @@ class IntegrationProcessTest < ActionController::IntegrationTest
cookies["cookie_3"] = "chocolate"
render :text => "Gone", :status => 410
end
+
+ def redirect
+ redirect_to :action => "get"
+ end
end
def test_get
@@ -274,6 +285,9 @@ class IntegrationProcessTest < ActionController::IntegrationTest
assert_equal "OK", status_message
assert_equal "200 OK", response.headers["Status"]
assert_equal ["200 OK"], headers["status"]
+ assert_response 200
+ assert_response :success
+ assert_response :ok
assert_equal [], response.headers["cookie"]
assert_equal [], headers["cookie"]
assert_equal({}, cookies)
@@ -290,6 +304,9 @@ class IntegrationProcessTest < ActionController::IntegrationTest
assert_equal "Created", status_message
assert_equal "201 Created", response.headers["Status"]
assert_equal ["201 Created"], headers["status"]
+ assert_response 201
+ assert_response :success
+ assert_response :created
assert_equal [], response.headers["cookie"]
assert_equal [], headers["cookie"]
assert_equal({}, cookies)
@@ -308,23 +325,84 @@ class IntegrationProcessTest < ActionController::IntegrationTest
assert_equal "Gone", status_message
assert_equal "410 Gone", response.headers["Status"]
assert_equal ["410 Gone"], headers["status"]
- assert_equal nil, response.headers["Set-Cookie"]
+ assert_response 410
+ assert_response :gone
+ assert_equal ["cookie_1=; path=/", "cookie_3=chocolate; path=/"], response.headers["Set-Cookie"]
assert_equal ["cookie_1=; path=/", "cookie_3=chocolate; path=/"], headers['set-cookie']
- assert_equal [[], ["chocolate"]], response.headers["cookie"]
+ assert_equal [
+ CGI::Cookie::new("name" => "cookie_1", "value" => ""),
+ CGI::Cookie::new("name" => "cookie_3", "value" => "chocolate")
+ ], response.headers["cookie"]
assert_equal [], headers["cookie"]
assert_equal({"cookie_1"=>"", "cookie_2"=>"oatmeal", "cookie_3"=>"chocolate"}, cookies)
assert_equal "Gone", response.body
end
end
+ def test_redirect
+ with_test_route_set do
+ get '/redirect'
+ assert_equal 302, status
+ assert_equal "Found", status_message
+ assert_equal "302 Found", response.headers["Status"]
+ assert_equal ["302 Found"], headers["status"]
+ assert_response 302
+ assert_response :redirect
+ assert_response :found
+ assert_equal "<html><body>You are being <a href=\"http://www.example.com/get\">redirected</a>.</body></html>", response.body
+ assert_kind_of HTML::Document, html_document
+ assert_equal 1, request_count
+ end
+ end
+
+ def test_xml_http_request_get
+ with_test_route_set do
+ xhr :get, '/get'
+ assert_equal 200, status
+ assert_equal "OK", status_message
+ assert_equal "200 OK", response.headers["Status"]
+ assert_equal ["200 OK"], headers["status"]
+ assert_response 200
+ assert_response :success
+ assert_response :ok
+ assert_equal "JS OK", response.body
+ end
+ end
+
+ def test_get_with_query_string
+ with_test_route_set do
+ get '/get_with_params?foo=bar'
+ assert_equal '/get_with_params?foo=bar', request.env["REQUEST_URI"]
+ assert_equal '/get_with_params?foo=bar', request.request_uri
+ assert_equal nil, request.env["QUERY_STRING"]
+ assert_equal 'foo=bar', request.query_string
+ assert_equal 'bar', request.parameters['foo']
+
+ assert_equal 200, status
+ assert_equal "foo: bar", response.body
+ end
+ end
+
+ def test_get_with_parameters
+ with_test_route_set do
+ get '/get_with_params', :foo => "bar"
+ assert_equal '/get_with_params', request.env["REQUEST_URI"]
+ assert_equal '/get_with_params', request.request_uri
+ assert_equal 'foo=bar', request.env["QUERY_STRING"]
+ assert_equal 'foo=bar', request.query_string
+ assert_equal 'bar', request.parameters['foo']
+
+ assert_equal 200, status
+ assert_equal "foo: bar", response.body
+ end
+ end
+
private
def with_test_route_set
with_routing do |set|
set.draw do |map|
map.with_options :controller => "IntegrationProcessTest::Integration" do |c|
- c.connect '/get', :action => "get"
- c.connect '/post', :action => "post"
- c.connect '/cookie_monster', :action => "cookie_monster"
+ c.connect "/:action"
end
end
yield
diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb
index 92b6aa4f2f..71f110f241 100644
--- a/actionpack/test/controller/layout_test.rb
+++ b/actionpack/test/controller/layout_test.rb
@@ -31,16 +31,8 @@ end
class MultipleExtensions < LayoutTest
end
-class MabView < ActionView::TemplateHandler
- def initialize(view)
- end
-
- def render(template, local_assigns)
- template.source
- end
-end
-
-ActionView::Template::register_template_handler :mab, MabView
+ActionView::Template::register_template_handler :mab,
+ lambda { |template| template.source.inspect }
class LayoutAutoDiscoveryTest < Test::Unit::TestCase
def setup
@@ -49,19 +41,19 @@ class LayoutAutoDiscoveryTest < Test::Unit::TestCase
@request.host = "www.nextangle.com"
end
-
+
def test_application_layout_is_default_when_no_controller_match
@controller = ProductController.new
get :hello
assert_equal 'layout_test.rhtml hello.rhtml', @response.body
end
-
+
def test_controller_name_layout_name_match
@controller = ItemController.new
get :hello
assert_equal 'item.rhtml hello.rhtml', @response.body
end
-
+
def test_third_party_template_library_auto_discovers_layout
ThirdPartyTemplateLibraryController.view_paths.reload!
@controller = ThirdPartyTemplateLibraryController.new
@@ -71,14 +63,14 @@ class LayoutAutoDiscoveryTest < Test::Unit::TestCase
assert_response :success
assert_equal 'Mab', @response.body
end
-
+
def test_namespaced_controllers_auto_detect_layouts
@controller = ControllerNameSpace::NestedController.new
get :hello
assert_equal 'layouts/controller_name_space/nested', @controller.active_layout
assert_equal 'controller_name_space/nested.rhtml hello.rhtml', @response.body
end
-
+
def test_namespaced_controllers_auto_detect_layouts
@controller = MultipleExtensions.new
get :hello
@@ -123,7 +115,7 @@ class ExemptFromLayoutTest < Test::Unit::TestCase
def test_rhtml_exempt_from_layout_status_should_prevent_layout_render
ActionController::Base.exempt_from_layout :rhtml
-
+
assert @controller.send!(:template_exempt_from_layout?, 'test.rhtml')
assert @controller.send!(:template_exempt_from_layout?, 'hello.rhtml')
@@ -164,19 +156,19 @@ class LayoutSetInResponseTest < Test::Unit::TestCase
get :hello
assert_equal 'layouts/layout_test', @response.layout
end
-
+
def test_layout_set_when_set_in_controller
@controller = HasOwnLayoutController.new
get :hello
assert_equal 'layouts/item', @response.layout
end
-
+
def test_layout_set_when_using_render
@controller = SetsLayoutInRenderController.new
get :hello
assert_equal 'layouts/third_party_template_library', @response.layout
end
-
+
def test_layout_is_not_set_when_none_rendered
@controller = RendersNoLayoutController.new
get :hello
@@ -257,4 +249,3 @@ class LayoutSymlinkedIsRenderedTest < Test::Unit::TestCase
assert_equal "layouts/symlinked/symlinked_layout", @response.layout
end
end
- \ No newline at end of file
diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb
index 1701431858..0d508eb8df 100644
--- a/actionpack/test/controller/mime_responds_test.rb
+++ b/actionpack/test/controller/mime_responds_test.rb
@@ -177,7 +177,7 @@ class MimeControllerTest < Test::Unit::TestCase
end
def test_html
- @request.env["HTTP_ACCEPT"] = "text/html"
+ @request.accept = "text/html"
get :js_or_html
assert_equal 'HTML', @response.body
@@ -189,7 +189,7 @@ class MimeControllerTest < Test::Unit::TestCase
end
def test_all
- @request.env["HTTP_ACCEPT"] = "*/*"
+ @request.accept = "*/*"
get :js_or_html
assert_equal 'HTML', @response.body # js is not part of all
@@ -201,13 +201,13 @@ class MimeControllerTest < Test::Unit::TestCase
end
def test_xml
- @request.env["HTTP_ACCEPT"] = "application/xml"
+ @request.accept = "application/xml"
get :html_xml_or_rss
assert_equal 'XML', @response.body
end
def test_js_or_html
- @request.env["HTTP_ACCEPT"] = "text/javascript, text/html"
+ @request.accept = "text/javascript, text/html"
get :js_or_html
assert_equal 'JS', @response.body
@@ -232,7 +232,7 @@ class MimeControllerTest < Test::Unit::TestCase
'JSON' => %w(application/json text/x-json)
}.each do |body, content_types|
content_types.each do |content_type|
- @request.env['HTTP_ACCEPT'] = content_type
+ @request.accept = content_type
get :json_or_yaml
assert_equal body, @response.body
end
@@ -240,7 +240,7 @@ class MimeControllerTest < Test::Unit::TestCase
end
def test_js_or_anything
- @request.env["HTTP_ACCEPT"] = "text/javascript, */*"
+ @request.accept = "text/javascript, */*"
get :js_or_html
assert_equal 'JS', @response.body
@@ -252,34 +252,34 @@ class MimeControllerTest < Test::Unit::TestCase
end
def test_using_defaults
- @request.env["HTTP_ACCEPT"] = "*/*"
+ @request.accept = "*/*"
get :using_defaults
assert_equal "text/html", @response.content_type
assert_equal 'Hello world!', @response.body
- @request.env["HTTP_ACCEPT"] = "text/javascript"
+ @request.accept = "text/javascript"
get :using_defaults
assert_equal "text/javascript", @response.content_type
assert_equal '$("body").visualEffect("highlight");', @response.body
- @request.env["HTTP_ACCEPT"] = "application/xml"
+ @request.accept = "application/xml"
get :using_defaults
assert_equal "application/xml", @response.content_type
assert_equal "<p>Hello world!</p>\n", @response.body
end
def test_using_defaults_with_type_list
- @request.env["HTTP_ACCEPT"] = "*/*"
+ @request.accept = "*/*"
get :using_defaults_with_type_list
assert_equal "text/html", @response.content_type
assert_equal 'Hello world!', @response.body
- @request.env["HTTP_ACCEPT"] = "text/javascript"
+ @request.accept = "text/javascript"
get :using_defaults_with_type_list
assert_equal "text/javascript", @response.content_type
assert_equal '$("body").visualEffect("highlight");', @response.body
- @request.env["HTTP_ACCEPT"] = "application/xml"
+ @request.accept = "application/xml"
get :using_defaults_with_type_list
assert_equal "application/xml", @response.content_type
assert_equal "<p>Hello world!</p>\n", @response.body
@@ -298,55 +298,55 @@ class MimeControllerTest < Test::Unit::TestCase
end
def test_synonyms
- @request.env["HTTP_ACCEPT"] = "application/javascript"
+ @request.accept = "application/javascript"
get :js_or_html
assert_equal 'JS', @response.body
- @request.env["HTTP_ACCEPT"] = "application/x-xml"
+ @request.accept = "application/x-xml"
get :html_xml_or_rss
assert_equal "XML", @response.body
end
def test_custom_types
- @request.env["HTTP_ACCEPT"] = "application/crazy-xml"
+ @request.accept = "application/crazy-xml"
get :custom_type_handling
assert_equal "application/crazy-xml", @response.content_type
assert_equal 'Crazy XML', @response.body
- @request.env["HTTP_ACCEPT"] = "text/html"
+ @request.accept = "text/html"
get :custom_type_handling
assert_equal "text/html", @response.content_type
assert_equal 'HTML', @response.body
end
def test_xhtml_alias
- @request.env["HTTP_ACCEPT"] = "application/xhtml+xml,application/xml"
+ @request.accept = "application/xhtml+xml,application/xml"
get :html_or_xml
assert_equal 'HTML', @response.body
end
def test_firefox_simulation
- @request.env["HTTP_ACCEPT"] = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
+ @request.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
get :html_or_xml
assert_equal 'HTML', @response.body
end
def test_handle_any
- @request.env["HTTP_ACCEPT"] = "*/*"
+ @request.accept = "*/*"
get :handle_any
assert_equal 'HTML', @response.body
- @request.env["HTTP_ACCEPT"] = "text/javascript"
+ @request.accept = "text/javascript"
get :handle_any
assert_equal 'Either JS or XML', @response.body
- @request.env["HTTP_ACCEPT"] = "text/xml"
+ @request.accept = "text/xml"
get :handle_any
assert_equal 'Either JS or XML', @response.body
end
def test_handle_any_any
- @request.env["HTTP_ACCEPT"] = "*/*"
+ @request.accept = "*/*"
get :handle_any_any
assert_equal 'HTML', @response.body
end
@@ -357,31 +357,31 @@ class MimeControllerTest < Test::Unit::TestCase
end
def test_handle_any_any_explicit_html
- @request.env["HTTP_ACCEPT"] = "text/html"
+ @request.accept = "text/html"
get :handle_any_any
assert_equal 'HTML', @response.body
end
def test_handle_any_any_javascript
- @request.env["HTTP_ACCEPT"] = "text/javascript"
+ @request.accept = "text/javascript"
get :handle_any_any
assert_equal 'Whatever you ask for, I got it', @response.body
end
def test_handle_any_any_xml
- @request.env["HTTP_ACCEPT"] = "text/xml"
+ @request.accept = "text/xml"
get :handle_any_any
assert_equal 'Whatever you ask for, I got it', @response.body
end
def test_rjs_type_skips_layout
- @request.env["HTTP_ACCEPT"] = "text/javascript"
+ @request.accept = "text/javascript"
get :all_types_with_layout
assert_equal 'RJS for all_types_with_layout', @response.body
end
def test_html_type_with_layout
- @request.env["HTTP_ACCEPT"] = "text/html"
+ @request.accept = "text/html"
get :all_types_with_layout
assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body
end
@@ -460,7 +460,7 @@ class MimeControllerTest < Test::Unit::TestCase
end
def test_format_with_custom_response_type_and_request_headers
- @request.env["HTTP_ACCEPT"] = "text/iphone"
+ @request.accept = "text/iphone"
get :iphone_with_html_response_type
assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
assert_equal "text/html", @response.content_type
@@ -470,7 +470,7 @@ class MimeControllerTest < Test::Unit::TestCase
get :iphone_with_html_response_type_without_layout
assert_equal '<html><div id="html_missing">Hello future from Firefox!</div></html>', @response.body
- @request.env["HTTP_ACCEPT"] = "text/iphone"
+ @request.accept = "text/iphone"
assert_raises(ActionView::MissingTemplate) { get :iphone_with_html_response_type_without_layout }
end
end
@@ -522,7 +522,7 @@ class MimeControllerLayoutsTest < Test::Unit::TestCase
get :index
assert_equal '<html><div id="html">Hello Firefox</div></html>', @response.body
- @request.env["HTTP_ACCEPT"] = "text/iphone"
+ @request.accept = "text/iphone"
get :index
assert_equal 'Hello iPhone', @response.body
end
@@ -533,7 +533,7 @@ class MimeControllerLayoutsTest < Test::Unit::TestCase
get :index
assert_equal 'Super Firefox', @response.body
- @request.env["HTTP_ACCEPT"] = "text/iphone"
+ @request.accept = "text/iphone"
get :index
assert_equal '<html><div id="super_iphone">Super iPhone</div></html>', @response.body
end
diff --git a/actionpack/test/controller/new_render_test.rb b/actionpack/test/controller/new_render_test.rb
index d2a3a2b0b0..82919b7777 100644
--- a/actionpack/test/controller/new_render_test.rb
+++ b/actionpack/test/controller/new_render_test.rb
@@ -136,6 +136,10 @@ class NewRenderTestController < ActionController::Base
render :partial => "partial_only", :layout => true
end
+ def partial_with_counter
+ render :partial => "counter", :locals => { :counter_counter => 5 }
+ end
+
def partial_with_locals
render :partial => "customer", :locals => { :customer => Customer.new("david") }
end
@@ -431,6 +435,10 @@ class NewRenderTestController < ActionController::Base
render :action => "using_layout_around_block"
end
+ def render_using_layout_around_block_with_args
+ render :action => "using_layout_around_block_with_args"
+ end
+
def render_using_layout_around_block_in_main_layout_and_within_content_for_layout
render :action => "using_layout_around_block"
end
@@ -741,6 +749,11 @@ EOS
assert_equal "<title>Talking to the layout</title>\nAction was here!", @response.body
end
+ def test_partial_with_counter
+ get :partial_with_counter
+ assert_equal "5", @response.body
+ end
+
def test_partials_list
get :partials_list
assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body
@@ -960,4 +973,9 @@ EOS
get :render_using_layout_around_block_in_main_layout_and_within_content_for_layout
assert_equal "Before (Anthony)\nInside from first block in layout\nAfter\nBefore (David)\nInside from block\nAfter\nBefore (Ramm)\nInside from second block in layout\nAfter\n", @response.body
end
+
+ def test_using_layout_around_block_with_args
+ get :render_using_layout_around_block_with_args
+ assert_equal "Before\narg1arg2\nAfter", @response.body
+ end
end
diff --git a/actionpack/test/controller/polymorphic_routes_test.rb b/actionpack/test/controller/polymorphic_routes_test.rb
index 3f52526f08..6ddf2826cd 100644
--- a/actionpack/test/controller/polymorphic_routes_test.rb
+++ b/actionpack/test/controller/polymorphic_routes_test.rb
@@ -60,6 +60,18 @@ uses_mocha 'polymorphic URL helpers' do
edit_polymorphic_url(@article)
end
+ def test_url_helper_prefixed_with_edit_with_url_options
+ @article.save
+ expects(:edit_article_url).with(@article, :param1 => '10')
+ edit_polymorphic_url(@article, :param1 => '10')
+ end
+
+ def test_url_helper_with_url_options
+ @article.save
+ expects(:article_url).with(@article, :param1 => '10')
+ polymorphic_url(@article, :param1 => '10')
+ end
+
def test_formatted_url_helper
expects(:formatted_article_url).with(@article, :pdf)
formatted_polymorphic_url([@article, :pdf])
@@ -67,10 +79,16 @@ uses_mocha 'polymorphic URL helpers' do
def test_format_option
@article.save
- expects(:article_url).with(@article, :pdf)
+ expects(:formatted_article_url).with(@article, :pdf)
polymorphic_url(@article, :format => :pdf)
end
+ def test_format_option_with_url_options
+ @article.save
+ expects(:formatted_article_url).with(@article, :pdf, :param1 => '10')
+ polymorphic_url(@article, :format => :pdf, :param1 => '10')
+ end
+
def test_id_and_format_option
@article.save
expects(:article_url).with(:id => @article, :format => :pdf)
@@ -147,7 +165,7 @@ uses_mocha 'polymorphic URL helpers' do
def test_nesting_with_array_containing_singleton_resource_and_format_option
@tag = Tag.new
@tag.save
- expects(:article_response_tag_url).with(@article, @tag, :pdf)
+ expects(:formatted_article_response_tag_url).with(@article, @tag, :pdf)
polymorphic_url([@article, :response, @tag], :format => :pdf)
end
diff --git a/actionpack/test/controller/rack_test.rb b/actionpack/test/controller/rack_test.rb
index 486fe49737..d5e56b9584 100644
--- a/actionpack/test/controller/rack_test.rb
+++ b/actionpack/test/controller/rack_test.rb
@@ -51,62 +51,74 @@ class BaseRackTest < Test::Unit::TestCase
end
def default_test; end
+
+ private
+
+ def set_content_data(data)
+ @request.env['REQUEST_METHOD'] = 'POST'
+ @request.env['CONTENT_LENGTH'] = data.length
+ @request.env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=utf-8'
+ @request.env['RAW_POST_DATA'] = data
+ end
end
class RackRequestTest < BaseRackTest
def test_proxy_request
- assert_equal 'glu.ttono.us', @request.host_with_port
+ assert_equal 'glu.ttono.us', @request.host_with_port(true)
end
def test_http_host
@env.delete "HTTP_X_FORWARDED_HOST"
@env['HTTP_HOST'] = "rubyonrails.org:8080"
- assert_equal "rubyonrails.org:8080", @request.host_with_port
+ assert_equal "rubyonrails.org", @request.host(true)
+ assert_equal "rubyonrails.org:8080", @request.host_with_port(true)
@env['HTTP_X_FORWARDED_HOST'] = "www.firsthost.org, www.secondhost.org"
- assert_equal "www.secondhost.org", @request.host
+ assert_equal "www.secondhost.org", @request.host(true)
end
def test_http_host_with_default_port_overrides_server_port
@env.delete "HTTP_X_FORWARDED_HOST"
@env['HTTP_HOST'] = "rubyonrails.org"
- assert_equal "rubyonrails.org", @request.host_with_port
+ assert_equal "rubyonrails.org", @request.host_with_port(true)
end
def test_host_with_port_defaults_to_server_name_if_no_host_headers
@env.delete "HTTP_X_FORWARDED_HOST"
@env.delete "HTTP_HOST"
- assert_equal "glu.ttono.us:8007", @request.host_with_port
+ assert_equal "glu.ttono.us:8007", @request.host_with_port(true)
end
def test_host_with_port_falls_back_to_server_addr_if_necessary
@env.delete "HTTP_X_FORWARDED_HOST"
@env.delete "HTTP_HOST"
@env.delete "SERVER_NAME"
- assert_equal "207.7.108.53:8007", @request.host_with_port
+ assert_equal "207.7.108.53", @request.host(true)
+ assert_equal 8007, @request.port(true)
+ assert_equal "207.7.108.53:8007", @request.host_with_port(true)
end
def test_host_with_port_if_http_standard_port_is_specified
@env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:80"
- assert_equal "glu.ttono.us", @request.host_with_port
+ assert_equal "glu.ttono.us", @request.host_with_port(true)
end
def test_host_with_port_if_https_standard_port_is_specified
@env['HTTP_X_FORWARDED_PROTO'] = "https"
@env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:443"
- assert_equal "glu.ttono.us", @request.host_with_port
+ assert_equal "glu.ttono.us", @request.host_with_port(true)
end
def test_host_if_ipv6_reference
@env.delete "HTTP_X_FORWARDED_HOST"
@env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]"
- assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host
+ assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host(true)
end
def test_host_if_ipv6_reference_with_port
@env.delete "HTTP_X_FORWARDED_HOST"
@env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]:8008"
- assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host
+ assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host(true)
end
def test_cgi_environment_variables
@@ -153,10 +165,8 @@ end
class RackRequestParamsParsingTest < BaseRackTest
def test_doesnt_break_when_content_type_has_charset
- data = 'flamenco=love'
- @request.env['CONTENT_LENGTH'] = data.length
- @request.env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=utf-8'
- @request.env['RAW_POST_DATA'] = data
+ set_content_data 'flamenco=love'
+
assert_equal({"flamenco"=> "love"}, @request.request_parameters)
end
@@ -166,6 +176,41 @@ class RackRequestParamsParsingTest < BaseRackTest
end
end
+class RackRequestContentTypeTest < BaseRackTest
+ def test_html_content_type_verification
+ @request.env['CONTENT_TYPE'] = Mime::HTML.to_s
+ assert @request.content_type.verify_request?
+ end
+
+ def test_xml_content_type_verification
+ @request.env['CONTENT_TYPE'] = Mime::XML.to_s
+ assert !@request.content_type.verify_request?
+ end
+end
+
+class RackRequestMethodTest < BaseRackTest
+ def test_get
+ assert_equal :get, @request.request_method
+ end
+
+ def test_post
+ @request.env['REQUEST_METHOD'] = 'POST'
+ assert_equal :post, @request.request_method
+ end
+
+ def test_put
+ set_content_data '_method=put'
+
+ assert_equal :put, @request.request_method
+ end
+
+ def test_delete
+ set_content_data '_method=delete'
+
+ assert_equal :delete, @request.request_method
+ end
+end
+
class RackRequestNeedsRewoundTest < BaseRackTest
def test_body_should_be_rewound
data = 'foo'
@@ -191,10 +236,17 @@ class RackResponseTest < BaseRackTest
def test_simple_output
@response.body = "Hello, World!"
+ @response.prepare!
status, headers, body = @response.out(@output)
assert_equal "200 OK", status
- assert_equal({"Content-Type" => "text/html", "Cache-Control" => "no-cache", "Set-Cookie" => []}, headers)
+ assert_equal({
+ "Content-Type" => "text/html; charset=utf-8",
+ "Cache-Control" => "private, max-age=0, must-revalidate",
+ "ETag" => '"65a8e27d8879283831b664bd8b7f0ad4"',
+ "Set-Cookie" => [],
+ "Content-Length" => "13"
+ }, headers)
parts = []
body.each { |part| parts << part }
@@ -205,10 +257,11 @@ class RackResponseTest < BaseRackTest
@response.body = Proc.new do |response, output|
5.times { |n| output.write(n) }
end
+ @response.prepare!
status, headers, body = @response.out(@output)
assert_equal "200 OK", status
- assert_equal({"Content-Type" => "text/html", "Cache-Control" => "no-cache", "Set-Cookie" => []}, headers)
+ assert_equal({"Content-Type" => "text/html; charset=utf-8", "Cache-Control" => "no-cache", "Set-Cookie" => []}, headers)
parts = []
body.each { |part| parts << part }
@@ -220,13 +273,16 @@ class RackResponseTest < BaseRackTest
@request.cgi.send :instance_variable_set, '@output_cookies', [cookie]
@response.body = "Hello, World!"
+ @response.prepare!
status, headers, body = @response.out(@output)
assert_equal "200 OK", status
assert_equal({
- "Content-Type" => "text/html",
- "Cache-Control" => "no-cache",
- "Set-Cookie" => ["name=Josh; path="]
+ "Content-Type" => "text/html; charset=utf-8",
+ "Cache-Control" => "private, max-age=0, must-revalidate",
+ "ETag" => '"65a8e27d8879283831b664bd8b7f0ad4"',
+ "Set-Cookie" => ["name=Josh; path="],
+ "Content-Length" => "13"
}, headers)
parts = []
@@ -234,3 +290,34 @@ class RackResponseTest < BaseRackTest
assert_equal ["Hello, World!"], parts
end
end
+
+class RackResponseHeadersTest < BaseRackTest
+ def setup
+ super
+ @response = ActionController::RackResponse.new(@request)
+ @output = StringIO.new('')
+ @response.headers['Status'] = "200 OK"
+ end
+
+ def test_content_type
+ [204, 304].each do |c|
+ @response.headers['Status'] = c.to_s
+ assert !response_headers.has_key?("Content-Type"), "#{c} should not have Content-Type header"
+ end
+
+ [200, 302, 404, 500].each do |c|
+ @response.headers['Status'] = c.to_s
+ assert response_headers.has_key?("Content-Type"), "#{c} did not have Content-Type header"
+ end
+ end
+
+ def test_status
+ assert !response_headers.has_key?('Status')
+ end
+
+ private
+ def response_headers
+ @response.prepare!
+ @response.out(@output)[1]
+ end
+end
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
index 28da5c6163..2f8bf7b6ee 100755..100644
--- a/actionpack/test/controller/redirect_test.rb
+++ b/actionpack/test/controller/redirect_test.rb
@@ -9,11 +9,11 @@ class Workshop
def initialize(id, new_record)
@id, @new_record = id, new_record
end
-
+
def new_record?
@new_record
end
-
+
def to_s
id.to_s
end
@@ -24,32 +24,32 @@ class RedirectController < ActionController::Base
redirect_to :action => "hello_world"
end
- def redirect_with_status
+ def redirect_with_status
redirect_to({:action => "hello_world", :status => 301})
- end
+ end
def redirect_with_status_hash
redirect_to({:action => "hello_world"}, {:status => 301})
- end
+ end
- def url_redirect_with_status
+ def url_redirect_with_status
redirect_to("http://www.example.com", :status => :moved_permanently)
- end
-
- def url_redirect_with_status_hash
+ end
+
+ def url_redirect_with_status_hash
redirect_to("http://www.example.com", {:status => 301})
- end
+ end
- def relative_url_redirect_with_status
+ def relative_url_redirect_with_status
redirect_to("/things/stuff", :status => :found)
- end
-
+ end
+
def relative_url_redirect_with_status_hash
redirect_to("/things/stuff", {:status => 301})
- end
-
- def redirect_to_back_with_status
- redirect_to :back, :status => 307
+ end
+
+ def redirect_to_back_with_status
+ redirect_to :back, :status => 307
end
def host_redirect
@@ -90,9 +90,9 @@ class RedirectController < ActionController::Base
end
def rescue_errors(e) raise e end
-
+
def rescue_action(e) raise end
-
+
protected
def dashbord_url(id, message)
url_for :action => "dashboard", :params => { "id" => id, "message" => message }
@@ -118,48 +118,48 @@ class RedirectTest < Test::Unit::TestCase
assert_equal "http://test.host/redirect/hello_world", redirect_to_url
end
- def test_redirect_with_status
- get :redirect_with_status
- assert_response 301
- assert_equal "http://test.host/redirect/hello_world", redirect_to_url
- end
+ def test_redirect_with_status
+ get :redirect_with_status
+ assert_response 301
+ assert_equal "http://test.host/redirect/hello_world", redirect_to_url
+ end
- def test_redirect_with_status_hash
+ def test_redirect_with_status_hash
get :redirect_with_status_hash
- assert_response 301
- assert_equal "http://test.host/redirect/hello_world", redirect_to_url
+ assert_response 301
+ assert_equal "http://test.host/redirect/hello_world", redirect_to_url
+ end
+
+ def test_url_redirect_with_status
+ get :url_redirect_with_status
+ assert_response 301
+ assert_equal "http://www.example.com", redirect_to_url
end
-
- def test_url_redirect_with_status
- get :url_redirect_with_status
- assert_response 301
- assert_equal "http://www.example.com", redirect_to_url
- end
def test_url_redirect_with_status_hash
get :url_redirect_with_status_hash
- assert_response 301
- assert_equal "http://www.example.com", redirect_to_url
- end
+ assert_response 301
+ assert_equal "http://www.example.com", redirect_to_url
+ end
-
- def test_relative_url_redirect_with_status
- get :relative_url_redirect_with_status
+
+ def test_relative_url_redirect_with_status
+ get :relative_url_redirect_with_status
assert_response 302
- assert_equal "http://test.host/things/stuff", redirect_to_url
- end
-
+ assert_equal "http://test.host/things/stuff", redirect_to_url
+ end
+
def test_relative_url_redirect_with_status_hash
get :relative_url_redirect_with_status_hash
- assert_response 301
- assert_equal "http://test.host/things/stuff", redirect_to_url
- end
-
- def test_redirect_to_back_with_status
- @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from"
- get :redirect_to_back_with_status
- assert_response 307
- assert_equal "http://www.example.com/coming/from", redirect_to_url
+ assert_response 301
+ assert_equal "http://test.host/things/stuff", redirect_to_url
+ end
+
+ def test_redirect_to_back_with_status
+ @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from"
+ get :redirect_to_back_with_status
+ assert_response 307
+ assert_equal "http://www.example.com/coming/from", redirect_to_url
end
def test_simple_redirect_using_options
@@ -204,20 +204,20 @@ class RedirectTest < Test::Unit::TestCase
assert_response :redirect
assert_equal "http://www.example.com/coming/from", redirect_to_url
end
-
+
def test_redirect_to_back_with_no_referer
assert_raises(ActionController::RedirectBackError) {
@request.env["HTTP_REFERER"] = nil
get :redirect_to_back
}
end
-
+
def test_redirect_to_record
ActionController::Routing::Routes.draw do |map|
map.resources :workshops
map.connect ':controller/:action/:id'
end
-
+
get :redirect_to_existing_record
assert_equal "http://test.host/workshops/5", redirect_to_url
assert_redirected_to Workshop.new(5, false)
@@ -237,7 +237,6 @@ class RedirectTest < Test::Unit::TestCase
get :redirect_to_nil
end
end
-
end
module ModuleTest
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index a857810b78..3008f5ca03 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -8,14 +8,23 @@ module Fun
end
end
-
-# FIXME: crashes Ruby 1.9
class TestController < ActionController::Base
layout :determine_layout
def hello_world
end
+ def conditional_hello
+ response.last_modified = Time.now.utc.beginning_of_day
+ response.etag = [:foo, 123]
+
+ if request.fresh?(response)
+ head :not_modified
+ else
+ render :action => 'hello_world'
+ end
+ end
+
def render_hello_world
render :template => "test/hello_world"
end
@@ -101,12 +110,7 @@ class TestController < ActionController::Base
end
def render_line_offset
- begin
- render :inline => '<% raise %>', :locals => {:foo => 'bar'}
- rescue RuntimeError => exc
- end
- line = exc.backtrace.first
- render :text => line
+ render :inline => '<% raise %>', :locals => {:foo => 'bar'}
end
def heading
@@ -198,11 +202,11 @@ class TestController < ActionController::Base
def render_alternate_default
# For this test, the method "default_render" is overridden:
- @alternate_default_render = lambda {
- render :update do |page|
- page.replace :foo, :partial => 'partial'
- end
- }
+ @alternate_default_render = lambda do
+ render :update do |page|
+ page.replace :foo, :partial => 'partial'
+ end
+ end
end
def rescue_action(e) raise end
@@ -238,10 +242,15 @@ class RenderTest < Test::Unit::TestCase
end
def test_line_offset
- get :render_line_offset
- line = @response.body
- assert(line =~ %r{:(\d+):})
- assert_equal "1", $1
+ begin
+ get :render_line_offset
+ flunk "the action should have raised an exception"
+ rescue RuntimeError => exc
+ line = exc.backtrace.first
+ assert(line =~ %r{:(\d+):})
+ assert_equal "1", $1,
+ "The line offset is wrong, perhaps the wrong exception has been raised, exception was: #{exc.inspect}"
+ end
end
def test_render_with_forward_slash
@@ -320,7 +329,7 @@ class RenderTest < Test::Unit::TestCase
def test_render_text_with_nil
get :render_text_with_nil
assert_response 200
- assert_equal '', @response.body
+ assert_equal ' ', @response.body
end
def test_render_text_with_false
@@ -408,58 +417,6 @@ class RenderTest < Test::Unit::TestCase
assert_equal "Goodbye, Local David", @response.body
end
- def test_render_200_should_set_etag
- get :render_hello_world_from_variable
- assert_equal etag_for("hello david"), @response.headers['ETag']
- assert_equal "private, max-age=0, must-revalidate", @response.headers['Cache-Control']
- end
-
- def test_render_against_etag_request_should_304_when_match
- @request.headers["HTTP_IF_NONE_MATCH"] = etag_for("hello david")
- get :render_hello_world_from_variable
- assert_equal "304 Not Modified", @response.headers['Status']
- assert @response.body.empty?
- end
-
- def test_render_against_etag_request_should_200_when_no_match
- @request.headers["HTTP_IF_NONE_MATCH"] = etag_for("hello somewhere else")
- get :render_hello_world_from_variable
- assert_equal "200 OK", @response.headers['Status']
- assert !@response.body.empty?
- end
-
- def test_render_with_etag
- get :render_hello_world_from_variable
- expected_etag = etag_for('hello david')
- assert_equal expected_etag, @response.headers['ETag']
-
- @request.headers["HTTP_IF_NONE_MATCH"] = expected_etag
- get :render_hello_world_from_variable
- assert_equal "304 Not Modified", @response.headers['Status']
-
- @request.headers["HTTP_IF_NONE_MATCH"] = "\"diftag\""
- get :render_hello_world_from_variable
- assert_equal "200 OK", @response.headers['Status']
- end
-
- def render_with_404_shouldnt_have_etag
- get :render_custom_code
- assert_nil @response.headers['ETag']
- end
-
- def test_etag_should_not_be_changed_when_already_set
- expected_etag = etag_for("hello somewhere else")
- @response.headers["ETag"] = expected_etag
- get :render_hello_world_from_variable
- assert_equal expected_etag, @response.headers['ETag']
- end
-
- def test_etag_should_govern_renders_with_layouts_too
- get :builder_layout_test
- assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body
- assert_equal etag_for("<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n"), @response.headers['ETag']
- end
-
def test_should_render_formatted_template
get :formatted_html_erb
assert_equal 'formatted html erb', @response.body
@@ -476,7 +433,7 @@ class RenderTest < Test::Unit::TestCase
end
def test_should_render_formatted_html_erb_template_with_faulty_accepts_header
- @request.env["HTTP_ACCEPT"] = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*"
+ @request.accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*"
get :formatted_xml_erb
assert_equal '<test>passed formatted html erb</test>', @response.body
end
@@ -516,8 +473,107 @@ class RenderTest < Test::Unit::TestCase
assert_equal "application/atomsvc+xml", @response.content_type
end
+ def test_should_use_implicit_content_type
+ get :implicit_content_type, :format => 'atom'
+ assert_equal Mime::ATOM, @response.content_type
+ end
+end
+
+class EtagRenderTest < Test::Unit::TestCase
+ def setup
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ @controller = TestController.new
+
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_render_200_should_set_etag
+ get :render_hello_world_from_variable
+ assert_equal etag_for("hello david"), @response.headers['ETag']
+ assert_equal "private, max-age=0, must-revalidate", @response.headers['Cache-Control']
+ end
+
+ def test_render_against_etag_request_should_304_when_match
+ @request.if_none_match = etag_for("hello david")
+ get :render_hello_world_from_variable
+ assert_equal "304 Not Modified", @response.status
+ assert @response.body.empty?
+ end
+
+ def test_render_against_etag_request_should_200_when_no_match
+ @request.if_none_match = etag_for("hello somewhere else")
+ get :render_hello_world_from_variable
+ assert_equal "200 OK", @response.status
+ assert !@response.body.empty?
+ end
+
+ def test_render_with_etag
+ get :render_hello_world_from_variable
+ expected_etag = etag_for('hello david')
+ assert_equal expected_etag, @response.headers['ETag']
+
+ @request.if_none_match = expected_etag
+ get :render_hello_world_from_variable
+ assert_equal "304 Not Modified", @response.status
+
+ @request.if_none_match = "\"diftag\""
+ get :render_hello_world_from_variable
+ assert_equal "200 OK", @response.status
+ end
+
+ def render_with_404_shouldnt_have_etag
+ get :render_custom_code
+ assert_nil @response.headers['ETag']
+ end
+
+ def test_etag_should_not_be_changed_when_already_set
+ expected_etag = etag_for("hello somewhere else")
+ @response.headers["ETag"] = expected_etag
+ get :render_hello_world_from_variable
+ assert_equal expected_etag, @response.headers['ETag']
+ end
+
+ def test_etag_should_govern_renders_with_layouts_too
+ get :builder_layout_test
+ assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body
+ assert_equal etag_for("<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n"), @response.headers['ETag']
+ end
+
protected
def etag_for(text)
%("#{Digest::MD5.hexdigest(text)}")
end
end
+
+class LastModifiedRenderTest < Test::Unit::TestCase
+ def setup
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ @controller = TestController.new
+
+ @request.host = "www.nextangle.com"
+ @last_modified = Time.now.utc.beginning_of_day.httpdate
+ end
+
+ def test_responds_with_last_modified
+ get :conditional_hello
+ assert_equal @last_modified, @response.headers['Last-Modified']
+ end
+
+ def test_request_not_modified
+ @request.if_modified_since = @last_modified
+ get :conditional_hello
+ assert_equal "304 Not Modified", @response.status
+ assert @response.body.blank?, @response.body
+ assert_equal @last_modified, @response.headers['Last-Modified']
+ end
+
+ def test_request_modified
+ @request.if_modified_since = 'Thu, 16 Jul 2008 00:00:00 GMT'
+ get :conditional_hello
+ assert_equal "200 OK", @response.status
+ assert !@response.body.blank?
+ assert_equal @last_modified, @response.headers['Last-Modified']
+ end
+end
diff --git a/actionpack/test/controller/request_test.rb b/actionpack/test/controller/request_test.rb
index 932c0e21a1..045dab4141 100644
--- a/actionpack/test/controller/request_test.rb
+++ b/actionpack/test/controller/request_test.rb
@@ -3,64 +3,69 @@ require 'action_controller/integration'
class RequestTest < Test::Unit::TestCase
def setup
+ ActionController::Base.relative_url_root = nil
@request = ActionController::TestRequest.new
end
+ def teardown
+ ActionController::Base.relative_url_root = nil
+ end
+
def test_remote_ip
assert_equal '0.0.0.0', @request.remote_ip
@request.remote_addr = '1.2.3.4'
- assert_equal '1.2.3.4', @request.remote_ip
+ assert_equal '1.2.3.4', @request.remote_ip(true)
@request.env['HTTP_CLIENT_IP'] = '2.3.4.5'
- assert_equal '1.2.3.4', @request.remote_ip
+ assert_equal '1.2.3.4', @request.remote_ip(true)
@request.remote_addr = '192.168.0.1'
- assert_equal '2.3.4.5', @request.remote_ip
+ assert_equal '2.3.4.5', @request.remote_ip(true)
@request.env.delete 'HTTP_CLIENT_IP'
@request.remote_addr = '1.2.3.4'
@request.env['HTTP_X_FORWARDED_FOR'] = '3.4.5.6'
- assert_equal '1.2.3.4', @request.remote_ip
+ assert_equal '1.2.3.4', @request.remote_ip(true)
@request.remote_addr = '127.0.0.1'
@request.env['HTTP_X_FORWARDED_FOR'] = '3.4.5.6'
- assert_equal '3.4.5.6', @request.remote_ip
+ assert_equal '3.4.5.6', @request.remote_ip(true)
@request.env['HTTP_X_FORWARDED_FOR'] = 'unknown,3.4.5.6'
- assert_equal '3.4.5.6', @request.remote_ip
+ assert_equal '3.4.5.6', @request.remote_ip(true)
@request.env['HTTP_X_FORWARDED_FOR'] = '172.16.0.1,3.4.5.6'
- assert_equal '3.4.5.6', @request.remote_ip
+ assert_equal '3.4.5.6', @request.remote_ip(true)
@request.env['HTTP_X_FORWARDED_FOR'] = '192.168.0.1,3.4.5.6'
- assert_equal '3.4.5.6', @request.remote_ip
+ assert_equal '3.4.5.6', @request.remote_ip(true)
@request.env['HTTP_X_FORWARDED_FOR'] = '10.0.0.1,3.4.5.6'
- assert_equal '3.4.5.6', @request.remote_ip
-
+ assert_equal '3.4.5.6', @request.remote_ip(true)
+
@request.env['HTTP_X_FORWARDED_FOR'] = '10.0.0.1, 10.0.0.1, 3.4.5.6'
- assert_equal '3.4.5.6', @request.remote_ip
+ assert_equal '3.4.5.6', @request.remote_ip(true)
@request.env['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,3.4.5.6'
- assert_equal '3.4.5.6', @request.remote_ip
+ assert_equal '3.4.5.6', @request.remote_ip(true)
@request.env['HTTP_X_FORWARDED_FOR'] = 'unknown,192.168.0.1'
- assert_equal 'unknown', @request.remote_ip
+ assert_equal 'unknown', @request.remote_ip(true)
@request.env['HTTP_X_FORWARDED_FOR'] = '9.9.9.9, 3.4.5.6, 10.0.0.1, 172.31.4.4'
- assert_equal '3.4.5.6', @request.remote_ip
+ assert_equal '3.4.5.6', @request.remote_ip(true)
@request.env['HTTP_CLIENT_IP'] = '8.8.8.8'
e = assert_raises(ActionController::ActionControllerError) {
- @request.remote_ip
+ @request.remote_ip(true)
}
assert_match /IP spoofing attack/, e.message
assert_match /HTTP_X_FORWARDED_FOR="9.9.9.9, 3.4.5.6, 10.0.0.1, 172.31.4.4"/, e.message
assert_match /HTTP_CLIENT_IP="8.8.8.8"/, e.message
@request.env['HTTP_X_FORWARDED_FOR'] = '8.8.8.8, 9.9.9.9'
- assert_equal '8.8.8.8', @request.remote_ip
+ assert_equal '8.8.8.8', @request.remote_ip(true)
@request.env.delete 'HTTP_CLIENT_IP'
@request.env.delete 'HTTP_X_FORWARDED_FOR'
@@ -120,169 +125,118 @@ class RequestTest < Test::Unit::TestCase
assert_equal ":8080", @request.port_string
end
- def test_relative_url_root
- @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi"
- @request.env['SERVER_SOFTWARE'] = 'lighttpd/1.2.3'
- assert_equal '', @request.relative_url_root, "relative_url_root should be disabled on lighttpd"
-
- @request.env['SERVER_SOFTWARE'] = 'apache/1.2.3 some random text'
-
- @request.env['SCRIPT_NAME'] = nil
- assert_equal "", @request.relative_url_root
-
- @request.env['SCRIPT_NAME'] = "/dispatch.cgi"
- assert_equal "", @request.relative_url_root
-
- @request.env['SCRIPT_NAME'] = "/myapp.rb"
- assert_equal "", @request.relative_url_root
-
- @request.relative_url_root = nil
- @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi"
- assert_equal "/hieraki", @request.relative_url_root
-
- @request.relative_url_root = nil
- @request.env['SCRIPT_NAME'] = "/collaboration/hieraki/dispatch.cgi"
- assert_equal "/collaboration/hieraki", @request.relative_url_root
-
- # apache/scgi case
- @request.relative_url_root = nil
- @request.env['SCRIPT_NAME'] = "/collaboration/hieraki"
- assert_equal "/collaboration/hieraki", @request.relative_url_root
-
- @request.relative_url_root = nil
- @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi"
- @request.env['SERVER_SOFTWARE'] = 'lighttpd/1.2.3'
- @request.env['RAILS_RELATIVE_URL_ROOT'] = "/hieraki"
- assert_equal "/hieraki", @request.relative_url_root
-
- # @env overrides path guess
- @request.relative_url_root = nil
- @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi"
- @request.env['SERVER_SOFTWARE'] = 'apache/1.2.3 some random text'
- @request.env['RAILS_RELATIVE_URL_ROOT'] = "/real_url"
- assert_equal "/real_url", @request.relative_url_root
- end
-
def test_request_uri
@request.env['SERVER_SOFTWARE'] = 'Apache 42.342.3432'
- @request.relative_url_root = nil
@request.set_REQUEST_URI "http://www.rubyonrails.org/path/of/some/uri?mapped=1"
assert_equal "/path/of/some/uri?mapped=1", @request.request_uri
assert_equal "/path/of/some/uri", @request.path
- @request.relative_url_root = nil
@request.set_REQUEST_URI "http://www.rubyonrails.org/path/of/some/uri"
assert_equal "/path/of/some/uri", @request.request_uri
assert_equal "/path/of/some/uri", @request.path
- @request.relative_url_root = nil
@request.set_REQUEST_URI "/path/of/some/uri"
assert_equal "/path/of/some/uri", @request.request_uri
assert_equal "/path/of/some/uri", @request.path
- @request.relative_url_root = nil
@request.set_REQUEST_URI "/"
assert_equal "/", @request.request_uri
assert_equal "/", @request.path
- @request.relative_url_root = nil
@request.set_REQUEST_URI "/?m=b"
assert_equal "/?m=b", @request.request_uri
assert_equal "/", @request.path
- @request.relative_url_root = nil
@request.set_REQUEST_URI "/"
@request.env['SCRIPT_NAME'] = "/dispatch.cgi"
assert_equal "/", @request.request_uri
assert_equal "/", @request.path
- @request.relative_url_root = nil
+ ActionController::Base.relative_url_root = "/hieraki"
@request.set_REQUEST_URI "/hieraki/"
@request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi"
assert_equal "/hieraki/", @request.request_uri
assert_equal "/", @request.path
+ ActionController::Base.relative_url_root = nil
- @request.relative_url_root = nil
+ ActionController::Base.relative_url_root = "/collaboration/hieraki"
@request.set_REQUEST_URI "/collaboration/hieraki/books/edit/2"
@request.env['SCRIPT_NAME'] = "/collaboration/hieraki/dispatch.cgi"
assert_equal "/collaboration/hieraki/books/edit/2", @request.request_uri
assert_equal "/books/edit/2", @request.path
+ ActionController::Base.relative_url_root = nil
# The following tests are for when REQUEST_URI is not supplied (as in IIS)
- @request.relative_url_root = nil
- @request.set_REQUEST_URI nil
@request.env['PATH_INFO'] = "/path/of/some/uri?mapped=1"
@request.env['SCRIPT_NAME'] = nil #"/path/dispatch.rb"
+ @request.set_REQUEST_URI nil
assert_equal "/path/of/some/uri?mapped=1", @request.request_uri
assert_equal "/path/of/some/uri", @request.path
- @request.set_REQUEST_URI nil
- @request.relative_url_root = nil
+ ActionController::Base.relative_url_root = '/path'
@request.env['PATH_INFO'] = "/path/of/some/uri?mapped=1"
@request.env['SCRIPT_NAME'] = "/path/dispatch.rb"
- assert_equal "/path/of/some/uri?mapped=1", @request.request_uri
- assert_equal "/of/some/uri", @request.path
-
@request.set_REQUEST_URI nil
- @request.relative_url_root = nil
+ assert_equal "/path/of/some/uri?mapped=1", @request.request_uri(true)
+ assert_equal "/of/some/uri", @request.path(true)
+ ActionController::Base.relative_url_root = nil
+
@request.env['PATH_INFO'] = "/path/of/some/uri"
@request.env['SCRIPT_NAME'] = nil
+ @request.set_REQUEST_URI nil
assert_equal "/path/of/some/uri", @request.request_uri
assert_equal "/path/of/some/uri", @request.path
- @request.set_REQUEST_URI nil
- @request.relative_url_root = nil
@request.env['PATH_INFO'] = "/"
+ @request.set_REQUEST_URI nil
assert_equal "/", @request.request_uri
assert_equal "/", @request.path
- @request.set_REQUEST_URI nil
- @request.relative_url_root = nil
@request.env['PATH_INFO'] = "/?m=b"
+ @request.set_REQUEST_URI nil
assert_equal "/?m=b", @request.request_uri
assert_equal "/", @request.path
- @request.set_REQUEST_URI nil
- @request.relative_url_root = nil
@request.env['PATH_INFO'] = "/"
@request.env['SCRIPT_NAME'] = "/dispatch.cgi"
+ @request.set_REQUEST_URI nil
assert_equal "/", @request.request_uri
assert_equal "/", @request.path
- @request.set_REQUEST_URI nil
- @request.relative_url_root = nil
+ ActionController::Base.relative_url_root = '/hieraki'
@request.env['PATH_INFO'] = "/hieraki/"
@request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi"
+ @request.set_REQUEST_URI nil
assert_equal "/hieraki/", @request.request_uri
assert_equal "/", @request.path
+ ActionController::Base.relative_url_root = nil
@request.set_REQUEST_URI '/hieraki/dispatch.cgi'
- @request.relative_url_root = '/hieraki'
- assert_equal "/dispatch.cgi", @request.path
- @request.relative_url_root = nil
+ ActionController::Base.relative_url_root = '/hieraki'
+ assert_equal "/dispatch.cgi", @request.path(true)
+ ActionController::Base.relative_url_root = nil
@request.set_REQUEST_URI '/hieraki/dispatch.cgi'
- @request.relative_url_root = '/foo'
- assert_equal "/hieraki/dispatch.cgi", @request.path
- @request.relative_url_root = nil
+ ActionController::Base.relative_url_root = '/foo'
+ assert_equal "/hieraki/dispatch.cgi", @request.path(true)
+ ActionController::Base.relative_url_root = nil
# This test ensures that Rails uses REQUEST_URI over PATH_INFO
- @request.relative_url_root = nil
+ ActionController::Base.relative_url_root = nil
@request.env['REQUEST_URI'] = "/some/path"
@request.env['PATH_INFO'] = "/another/path"
@request.env['SCRIPT_NAME'] = "/dispatch.cgi"
- assert_equal "/some/path", @request.request_uri
- assert_equal "/some/path", @request.path
+ assert_equal "/some/path", @request.request_uri(true)
+ assert_equal "/some/path", @request.path(true)
end
-
def test_host_with_default_port
@request.host = "rubyonrails.org"
@request.port = 80
assert_equal "rubyonrails.org", @request.host_with_port
end
-
+
def test_host_with_non_default_port
@request.host = "rubyonrails.org"
@request.port = 81
@@ -290,13 +244,13 @@ class RequestTest < Test::Unit::TestCase
end
def test_server_software
- assert_equal nil, @request.server_software
+ assert_equal nil, @request.server_software(true)
@request.env['SERVER_SOFTWARE'] = 'Apache3.422'
- assert_equal 'apache', @request.server_software
+ assert_equal 'apache', @request.server_software(true)
@request.env['SERVER_SOFTWARE'] = 'lighttpd(1.1.4)'
- assert_equal 'lighttpd', @request.server_software
+ assert_equal 'lighttpd', @request.server_software(true)
end
def test_xml_http_request
@@ -326,44 +280,44 @@ class RequestTest < Test::Unit::TestCase
def test_symbolized_request_methods
[:get, :post, :put, :delete].each do |method|
- set_request_method_to method
+ self.request_method = method
assert_equal method, @request.method
end
end
def test_invalid_http_method_raises_exception
- set_request_method_to :random_method
assert_raises(ActionController::UnknownHttpMethod) do
- @request.method
+ self.request_method = :random_method
end
end
def test_allow_method_hacking_on_post
- set_request_method_to :post
+ self.request_method = :post
[:get, :head, :options, :put, :post, :delete].each do |method|
- @request.instance_eval { @parameters = { :_method => method } ; @request_method = nil }
+ @request.instance_eval { @parameters = { :_method => method.to_s } ; @request_method = nil }
+ @request.request_method(true)
assert_equal(method == :head ? :get : method, @request.method)
end
end
def test_invalid_method_hacking_on_post_raises_exception
- set_request_method_to :post
+ self.request_method = :post
@request.instance_eval { @parameters = { :_method => :random_method } ; @request_method = nil }
assert_raises(ActionController::UnknownHttpMethod) do
- @request.method
+ @request.request_method(true)
end
end
def test_restrict_method_hacking
@request.instance_eval { @parameters = { :_method => 'put' } }
[:get, :put, :delete].each do |method|
- set_request_method_to method
+ self.request_method = method
assert_equal method, @request.method
end
end
- def test_head_masquarading_as_get
- set_request_method_to :head
+ def test_head_masquerading_as_get
+ self.request_method = :head
assert_equal :get, @request.method
assert @request.get?
assert @request.head?
@@ -385,9 +339,16 @@ class RequestTest < Test::Unit::TestCase
end
def test_nil_format
- @request.instance_eval { @parameters = { :format => nil } }
+ ActionController::Base.use_accept_header, old =
+ false, ActionController::Base.use_accept_header
+
+ @request.instance_eval { @parameters = {} }
@request.env["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
+ assert @request.xhr?
assert_equal Mime::JS, @request.format
+
+ ensure
+ ActionController::Base.use_accept_header = old
end
def test_content_type
@@ -415,28 +376,27 @@ class RequestTest < Test::Unit::TestCase
@request.env["CONTENT_TYPE"] = "application/xml; charset=UTF-8"
assert_equal Mime::XML, @request.content_type
end
-
+
def test_user_agent
assert_not_nil @request.user_agent
end
-
+
def test_parameters
@request.instance_eval { @request_parameters = { "foo" => 1 } }
@request.instance_eval { @query_parameters = { "bar" => 2 } }
-
+
assert_equal({"foo" => 1, "bar" => 2}, @request.parameters)
assert_equal({"foo" => 1}, @request.request_parameters)
assert_equal({"bar" => 2}, @request.query_parameters)
end
protected
- def set_request_method_to(method)
+ def request_method=(method)
@request.env['REQUEST_METHOD'] = method.to_s.upcase
- @request.instance_eval { @request_method = nil }
+ @request.request_method(true)
end
end
-
class UrlEncodedRequestParameterParsingTest < Test::Unit::TestCase
def setup
@query_string = "action=create_customer&full_name=David%20Heinemeier%20Hansson&customerId=1"
@@ -548,7 +508,6 @@ class UrlEncodedRequestParameterParsingTest < Test::Unit::TestCase
)
end
-
def test_request_hash_parsing
query = {
"note[viewers][viewer][][type]" => ["User", "Group"],
@@ -560,7 +519,6 @@ class UrlEncodedRequestParameterParsingTest < Test::Unit::TestCase
assert_equal(expected, ActionController::AbstractRequest.parse_request_parameters(query))
end
-
def test_parse_params
input = {
"customers[boston][first][name]" => [ "David" ],
@@ -743,7 +701,6 @@ class UrlEncodedRequestParameterParsingTest < Test::Unit::TestCase
end
end
-
class MultipartRequestParameterParsingTest < Test::Unit::TestCase
FIXTURE_PATH = File.dirname(__FILE__) + '/../fixtures/multipart'
@@ -774,19 +731,19 @@ class MultipartRequestParameterParsingTest < Test::Unit::TestCase
file = params['file']
foo = params['foo']
-
+
if RUBY_VERSION > '1.9'
assert_kind_of File, file
else
assert_kind_of Tempfile, file
end
-
+
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
-
+
assert_equal 'bar', foo
end
-
+
def test_large_text_file
params = process('large_text_file')
assert_equal %w(file foo), params.keys.sort
@@ -891,20 +848,20 @@ class XmlParamsParsingTest < Test::Unit::TestCase
private
def parse_body(body)
- env = { 'CONTENT_TYPE' => 'application/xml',
+ env = { 'rack.input' => StringIO.new(body),
+ 'CONTENT_TYPE' => 'application/xml',
'CONTENT_LENGTH' => body.size.to_s }
- cgi = ActionController::Integration::Session::StubCGI.new(env, body)
- ActionController::CgiRequest.new(cgi).request_parameters
+ ActionController::RackRequest.new(env).request_parameters
end
end
class LegacyXmlParamsParsingTest < XmlParamsParsingTest
private
def parse_body(body)
- env = { 'HTTP_X_POST_DATA_FORMAT' => 'xml',
- 'CONTENT_LENGTH' => body.size.to_s }
- cgi = ActionController::Integration::Session::StubCGI.new(env, body)
- ActionController::CgiRequest.new(cgi).request_parameters
+ env = { 'rack.input' => StringIO.new(body),
+ 'HTTP_X_POST_DATA_FORMAT' => 'xml',
+ 'CONTENT_LENGTH' => body.size.to_s }
+ ActionController::RackRequest.new(env).request_parameters
end
end
@@ -923,9 +880,9 @@ class JsonParamsParsingTest < Test::Unit::TestCase
private
def parse_body(body,content_type)
- env = { 'CONTENT_TYPE' => content_type,
+ env = { 'rack.input' => StringIO.new(body),
+ 'CONTENT_TYPE' => content_type,
'CONTENT_LENGTH' => body.size.to_s }
- cgi = ActionController::Integration::Session::StubCGI.new(env, body)
- ActionController::CgiRequest.new(cgi).request_parameters
+ ActionController::RackRequest.new(env).request_parameters
end
end
diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb
index 27fcc5e04c..da076d2090 100644
--- a/actionpack/test/controller/rescue_test.rb
+++ b/actionpack/test/controller/rescue_test.rb
@@ -62,6 +62,11 @@ class RescueController < ActionController::Base
render :text => exception.message
end
+ # This is a Dispatcher exception and should be in ApplicationController.
+ rescue_from ActionController::RoutingError do
+ render :text => 'no way'
+ end
+
def raises
render :text => 'already rendered'
raise "don't panic!"
@@ -378,6 +383,10 @@ class RescueTest < Test::Unit::TestCase
assert_equal "RescueController::ResourceUnavailableToRescueAsString", @response.body
end
+ def test_rescue_dispatcher_exceptions
+ RescueController.process_with_exception(@request, @response, ActionController::RoutingError.new("Route not found"))
+ assert_equal "no way", @response.body
+ end
protected
def with_all_requests_local(local = true)
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index 0f7924649a..e153b0cc98 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -516,6 +516,26 @@ class ResourcesTest < Test::Unit::TestCase
end
end
+ def test_should_not_allow_invalid_head_method_for_member_routes
+ with_routing do |set|
+ set.draw do |map|
+ assert_raises(ArgumentError) do
+ map.resources :messages, :member => {:something => :head}
+ end
+ end
+ end
+ end
+
+ def test_should_not_allow_invalid_http_methods_for_member_routes
+ with_routing do |set|
+ set.draw do |map|
+ assert_raises(ArgumentError) do
+ map.resources :messages, :member => {:something => :invalid}
+ end
+ end
+ end
+ end
+
def test_resource_action_separator
with_routing do |set|
set.draw do |map|
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index c5ccb71582..6cf134c26f 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -67,66 +67,56 @@ class SegmentTest < Test::Unit::TestCase
end
def test_interpolation_statement
- s = ROUTING::StaticSegment.new
- s.value = "Hello"
+ s = ROUTING::StaticSegment.new("Hello")
assert_equal "Hello", eval(s.interpolation_statement([]))
assert_equal "HelloHello", eval(s.interpolation_statement([s]))
- s2 = ROUTING::StaticSegment.new
- s2.value = "-"
+ s2 = ROUTING::StaticSegment.new("-")
assert_equal "Hello-Hello", eval(s.interpolation_statement([s, s2]))
- s3 = ROUTING::StaticSegment.new
- s3.value = "World"
+ s3 = ROUTING::StaticSegment.new("World")
assert_equal "Hello-World", eval(s3.interpolation_statement([s, s2]))
end
end
class StaticSegmentTest < Test::Unit::TestCase
def test_interpolation_chunk_should_respect_raw
- s = ROUTING::StaticSegment.new
- s.value = 'Hello World'
- assert ! s.raw?
+ s = ROUTING::StaticSegment.new('Hello World')
+ assert !s.raw?
assert_equal 'Hello%20World', s.interpolation_chunk
- s.raw = true
+ s = ROUTING::StaticSegment.new('Hello World', :raw => true)
assert s.raw?
assert_equal 'Hello World', s.interpolation_chunk
end
def test_regexp_chunk_should_escape_specials
- s = ROUTING::StaticSegment.new
-
- s.value = 'Hello*World'
+ s = ROUTING::StaticSegment.new('Hello*World')
assert_equal 'Hello\*World', s.regexp_chunk
- s.value = 'HelloWorld'
+ s = ROUTING::StaticSegment.new('HelloWorld')
assert_equal 'HelloWorld', s.regexp_chunk
end
def test_regexp_chunk_should_add_question_mark_for_optionals
- s = ROUTING::StaticSegment.new
- s.value = "/"
- s.is_optional = true
+ s = ROUTING::StaticSegment.new("/", :optional => true)
assert_equal "/?", s.regexp_chunk
- s.value = "hello"
+ s = ROUTING::StaticSegment.new("hello", :optional => true)
assert_equal "(?:hello)?", s.regexp_chunk
end
end
class DynamicSegmentTest < Test::Unit::TestCase
- def segment
+ def segment(options = {})
unless @segment
- @segment = ROUTING::DynamicSegment.new
- @segment.key = :a
+ @segment = ROUTING::DynamicSegment.new(:a, options)
end
@segment
end
def test_extract_value
- s = ROUTING::DynamicSegment.new
- s.key = :a
+ s = ROUTING::DynamicSegment.new(:a)
hash = {:a => '10', :b => '20'}
assert_equal '10', eval(s.extract_value)
@@ -149,31 +139,31 @@ class DynamicSegmentTest < Test::Unit::TestCase
end
def test_regexp_value_check_rejects_nil
- segment.regexp = /\d+/
+ segment = segment(:regexp => /\d+/)
+
a_value = nil
- assert ! eval(segment.value_check)
+ assert !eval(segment.value_check)
end
def test_optional_regexp_value_check_should_accept_nil
- segment.regexp = /\d+/
- segment.is_optional = true
+ segment = segment(:regexp => /\d+/, :optional => true)
+
a_value = nil
assert eval(segment.value_check)
end
def test_regexp_value_check_rejects_no_match
- segment.regexp = /\d+/
+ segment = segment(:regexp => /\d+/)
a_value = "Hello20World"
- assert ! eval(segment.value_check)
+ assert !eval(segment.value_check)
a_value = "20Hi"
- assert ! eval(segment.value_check)
+ assert !eval(segment.value_check)
end
def test_regexp_value_check_accepts_match
- segment.regexp = /\d+/
-
+ segment = segment(:regexp => /\d+/)
a_value = "30"
assert eval(segment.value_check)
end
@@ -184,14 +174,14 @@ class DynamicSegmentTest < Test::Unit::TestCase
end
def test_optional_value_needs_no_check
- segment.is_optional = true
+ segment = segment(:optional => true)
+
a_value = nil
assert_equal nil, segment.value_check
end
def test_regexp_value_check_should_accept_match_with_default
- segment.regexp = /\d+/
- segment.default = '200'
+ segment = segment(:regexp => /\d+/, :default => '200')
a_value = '100'
assert eval(segment.value_check)
@@ -234,7 +224,7 @@ class DynamicSegmentTest < Test::Unit::TestCase
end
def test_extraction_code_should_return_on_mismatch
- segment.regexp = /\d+/
+ segment = segment(:regexp => /\d+/)
hash = merged = {:a => 'Hi', :b => '3'}
options = {:b => '3'}
a_value = nil
@@ -292,7 +282,7 @@ class DynamicSegmentTest < Test::Unit::TestCase
end
def test_value_regexp_should_match_exacly
- segment.regexp = /\d+/
+ segment = segment(:regexp => /\d+/)
assert_no_match segment.value_regexp, "Hello 10 World"
assert_no_match segment.value_regexp, "Hello 10"
assert_no_match segment.value_regexp, "10 World"
@@ -300,40 +290,36 @@ class DynamicSegmentTest < Test::Unit::TestCase
end
def test_regexp_chunk_should_return_string
- segment.regexp = /\d+/
+ segment = segment(:regexp => /\d+/)
assert_kind_of String, segment.regexp_chunk
end
def test_build_pattern_non_optional_with_no_captures
# Non optional
- a_segment = ROUTING::DynamicSegment.new
- a_segment.regexp = /\d+/ #number_of_captures is 0
+ a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /\d+/)
assert_equal "(\\d+)stuff", a_segment.build_pattern('stuff')
end
def test_build_pattern_non_optional_with_captures
# Non optional
- a_segment = ROUTING::DynamicSegment.new
- a_segment.regexp = /(\d+)(.*?)/ #number_of_captures is 2
+ a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /(\d+)(.*?)/)
assert_equal "((\\d+)(.*?))stuff", a_segment.build_pattern('stuff')
end
def test_optionality_implied
- a_segment = ROUTING::DynamicSegment.new
- a_segment.key = :id
+ a_segment = ROUTING::DynamicSegment.new(:id)
assert a_segment.optionality_implied?
- a_segment.key = :action
+ a_segment = ROUTING::DynamicSegment.new(:action)
assert a_segment.optionality_implied?
end
def test_modifiers_must_be_handled_sensibly
- a_segment = ROUTING::DynamicSegment.new
- a_segment.regexp = /david|jamis/i
+ a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /david|jamis/i)
assert_equal "((?i-mx:david|jamis))stuff", a_segment.build_pattern('stuff')
- a_segment.regexp = /david|jamis/x
+ a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /david|jamis/x)
assert_equal "((?x-mi:david|jamis))stuff", a_segment.build_pattern('stuff')
- a_segment.regexp = /david|jamis/
+ a_segment = ROUTING::DynamicSegment.new(nil, :regexp => /david|jamis/)
assert_equal "(david|jamis)stuff", a_segment.build_pattern('stuff')
end
end
@@ -560,7 +546,7 @@ class RouteBuilderTest < Test::Unit::TestCase
action = segments[-4]
assert_equal :action, action.key
- action.regexp = /show|in/ # Use 'in' to check partial matches
+ segments[-4] = ROUTING::DynamicSegment.new(:action, :regexp => /show|in/)
builder.assign_default_route_options(segments)
@@ -661,10 +647,10 @@ class RoutingTest < Test::Unit::TestCase
ActionController::Routing.controller_paths = []
assert_equal [], ActionController::Routing.possible_controllers
- ActionController::Routing::Routes.load!
ActionController::Routing.controller_paths = [
root, root + '/app/controllers', root + '/vendor/plugins/bad_plugin/lib'
]
+ ActionController::Routing::Routes.load!
assert_equal ["admin/user", "plugin", "user"], ActionController::Routing.possible_controllers.sort
ensure
@@ -731,15 +717,10 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do
def request
@request ||= MockRequest.new(:host => "named.route.test", :method => :get)
end
-
- def relative_url_root=(value)
- request.relative_url_root=value
- end
end
class MockRequest
- attr_accessor :path, :path_parameters, :host, :subdomains, :domain,
- :method, :relative_url_root
+ attr_accessor :path, :path_parameters, :host, :subdomains, :domain, :method
def initialize(values={})
values.each { |key, value| send("#{key}=", value) }
@@ -839,6 +820,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do
puts "#{1 / per_url} url/s\n\n"
end
end
+
def test_time_generation
n = 5000
if RunTimeTests
@@ -920,10 +902,11 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do
def test_basic_named_route_with_relative_url_root
rs.add_named_route :home, '', :controller => 'content', :action => 'list'
x = setup_for_named_route
- x.relative_url_root="/foo"
+ ActionController::Base.relative_url_root = "/foo"
assert_equal("http://named.route.test/foo/",
x.send(:home_url))
assert_equal "/foo/", x.send(:home_path)
+ ActionController::Base.relative_url_root = nil
end
def test_named_route_with_option
@@ -1377,34 +1360,20 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do
end
def slash_segment(is_optional = false)
- returning ROUTING::DividerSegment.new('/') do |s|
- s.is_optional = is_optional
- end
+ ROUTING::DividerSegment.new('/', :optional => is_optional)
end
def default_route
unless defined?(@default_route)
- @default_route = ROUTING::Route.new
-
- @default_route.segments << (s = ROUTING::StaticSegment.new)
- s.value = '/'
- s.raw = true
-
- @default_route.segments << (s = ROUTING::DynamicSegment.new)
- s.key = :controller
-
- @default_route.segments << slash_segment(:optional)
- @default_route.segments << (s = ROUTING::DynamicSegment.new)
- s.key = :action
- s.default = 'index'
- s.is_optional = true
-
- @default_route.segments << slash_segment(:optional)
- @default_route.segments << (s = ROUTING::DynamicSegment.new)
- s.key = :id
- s.is_optional = true
-
- @default_route.segments << slash_segment(:optional)
+ segments = []
+ segments << ROUTING::StaticSegment.new('/', :raw => true)
+ segments << ROUTING::DynamicSegment.new(:controller)
+ segments << slash_segment(:optional)
+ segments << ROUTING::DynamicSegment.new(:action, :default => 'index', :optional => true)
+ segments << slash_segment(:optional)
+ segments << ROUTING::DynamicSegment.new(:id, :optional => true)
+ segments << slash_segment(:optional)
+ @default_route = ROUTING::Route.new(segments).freeze
end
@default_route
end
@@ -1492,29 +1461,16 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do
end
def test_significant_keys
- user_url = ROUTING::Route.new
- user_url.segments << (s = ROUTING::StaticSegment.new)
- s.value = '/'
- s.raw = true
-
- user_url.segments << (s = ROUTING::StaticSegment.new)
- s.value = 'user'
-
- user_url.segments << (s = ROUTING::StaticSegment.new)
- s.value = '/'
- s.raw = true
- s.is_optional = true
-
- user_url.segments << (s = ROUTING::DynamicSegment.new)
- s.key = :user
-
- user_url.segments << (s = ROUTING::StaticSegment.new)
- s.value = '/'
- s.raw = true
- s.is_optional = true
+ segments = []
+ segments << ROUTING::StaticSegment.new('/', :raw => true)
+ segments << ROUTING::StaticSegment.new('user')
+ segments << ROUTING::StaticSegment.new('/', :raw => true, :optional => true)
+ segments << ROUTING::DynamicSegment.new(:user)
+ segments << ROUTING::StaticSegment.new('/', :raw => true, :optional => true)
- user_url.requirements = {:controller => 'users', :action => 'show'}
+ requirements = {:controller => 'users', :action => 'show'}
+ user_url = ROUTING::Route.new(segments, requirements)
keys = user_url.significant_keys.sort_by { |k| k.to_s }
assert_equal [:action, :controller, :user], keys
end
@@ -1801,6 +1757,22 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do
end
end
+ def test_route_requirements_with_invalid_http_method_is_invalid
+ assert_raises ArgumentError do
+ set.draw do |map|
+ map.connect 'valid/route', :controller => 'pages', :action => 'show', :conditions => {:method => :invalid}
+ end
+ end
+ end
+
+ def test_route_requirements_with_head_method_condition_is_invalid
+ assert_raises ArgumentError do
+ set.draw do |map|
+ map.connect 'valid/route', :controller => 'pages', :action => 'show', :conditions => {:method => :head}
+ end
+ end
+ end
+
def test_non_path_route_requirements_match_all
set.draw do |map|
map.connect 'page/37s', :controller => 'pages', :action => 'show', :name => /(jamis|david)/
diff --git a/actionpack/test/controller/session/cookie_store_test.rb b/actionpack/test/controller/session/cookie_store_test.rb
index 5adaeaf5c5..5adaeaf5c5 100755..100644
--- a/actionpack/test/controller/session/cookie_store_test.rb
+++ b/actionpack/test/controller/session/cookie_store_test.rb
diff --git a/actionpack/test/controller/test_test.rb b/actionpack/test/controller/test_test.rb
index b624005a57..58d9ca537f 100644
--- a/actionpack/test/controller/test_test.rb
+++ b/actionpack/test/controller/test_test.rb
@@ -64,7 +64,7 @@ class TestTest < Test::Unit::TestCase
</html>
HTML
end
-
+
def test_xml_output
response.content_type = "application/xml"
render :text => <<XML
@@ -117,8 +117,8 @@ XML
@controller = TestController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
- ActionController::Routing::Routes.reload
ActionController::Routing.use_controllers! %w(content admin/user test_test/test)
+ ActionController::Routing::Routes.load_routes!
end
def teardown
@@ -366,7 +366,7 @@ XML
:children => { :count => 1,
:only => { :tag => "img" } } } }
end
-
+
def test_should_not_impose_childless_html_tags_in_xml
process :test_xml_output
@@ -412,7 +412,7 @@ XML
def test_assert_routing_with_method
with_routing do |set|
- set.draw { |map| map.resources(:content) }
+ set.draw { |map| map.resources(:content) }
assert_routing({ :method => 'post', :path => 'content' }, { :controller => 'content', :action => 'create' })
end
end
@@ -486,7 +486,7 @@ XML
assert_nil @request.env['HTTP_X_REQUESTED_WITH']
end
- def test_header_properly_reset_after_get_request
+ def test_header_properly_reset_after_get_request
get :test_params
@request.recycle!
assert_nil @request.instance_variable_get("@request_method")
@@ -532,15 +532,15 @@ XML
assert_equal file.path, file.local_path
assert_equal expected, file.read
end
-
+
def test_test_uploaded_file_with_binary
filename = 'mona_lisa.jpg'
path = "#{FILES_DIR}/#{filename}"
content_type = 'image/png'
-
+
binary_uploaded_file = ActionController::TestUploadedFile.new(path, content_type, :binary)
assert_equal File.open(path, READ_BINARY).read, binary_uploaded_file.read
-
+
plain_uploaded_file = ActionController::TestUploadedFile.new(path, content_type)
assert_equal File.open(path, READ_PLAIN).read, plain_uploaded_file.read
end
@@ -549,10 +549,10 @@ XML
filename = 'mona_lisa.jpg'
path = "#{FILES_DIR}/#{filename}"
content_type = 'image/jpg'
-
+
binary_file_upload = fixture_file_upload(path, content_type, :binary)
assert_equal File.open(path, READ_BINARY).read, binary_file_upload.read
-
+
plain_file_upload = fixture_file_upload(path, content_type)
assert_equal File.open(path, READ_PLAIN).read, plain_file_upload.read
end
@@ -584,7 +584,7 @@ XML
get :test_send_file
assert_nothing_raised(NoMethodError) { @response.binary_content }
end
-
+
protected
def with_foo_routing
with_routing do |set|
@@ -597,7 +597,6 @@ XML
end
end
-
class CleanBacktraceTest < Test::Unit::TestCase
def test_should_reraise_the_same_object
exception = Test::Unit::AssertionFailedError.new('message')
@@ -658,7 +657,7 @@ end
class NamedRoutesControllerTest < ActionController::TestCase
tests ContentController
-
+
def test_should_be_able_to_use_named_routes_before_a_request_is_done
with_routing do |set|
set.draw { |map| map.resources :contents }
diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb
index a9974db5d5..64e9a085ca 100644
--- a/actionpack/test/controller/url_rewriter_test.rb
+++ b/actionpack/test/controller/url_rewriter_test.rb
@@ -7,7 +7,7 @@ class UrlRewriterTests < Test::Unit::TestCase
@request = ActionController::TestRequest.new
@params = {}
@rewriter = ActionController::UrlRewriter.new(@request, @params)
- end
+ end
def test_port
assert_equal('http://test.host:1271/c/a/i',
@@ -24,7 +24,7 @@ class UrlRewriterTests < Test::Unit::TestCase
@rewriter.rewrite(:protocol => 'https://', :controller => 'c', :action => 'a', :id => 'i')
)
end
-
+
def test_user_name_and_password
assert_equal(
'http://david:secret@test.host/c/a/i',
@@ -38,12 +38,12 @@ class UrlRewriterTests < Test::Unit::TestCase
@rewriter.rewrite(:user => "openid.aol.com/nextangler", :password => "one two?", :controller => 'c', :action => 'a', :id => 'i')
)
end
-
- def test_anchor
- assert_equal(
- 'http://test.host/c/a/i#anchor',
- @rewriter.rewrite(:controller => 'c', :action => 'a', :id => 'i', :anchor => 'anchor')
- )
+
+ def test_anchor
+ assert_equal(
+ 'http://test.host/c/a/i#anchor',
+ @rewriter.rewrite(:controller => 'c', :action => 'a', :id => 'i', :anchor => 'anchor')
+ )
end
def test_overwrite_params
@@ -55,12 +55,12 @@ class UrlRewriterTests < Test::Unit::TestCase
u = @rewriter.rewrite(:only_path => false, :overwrite_params => {:action => 'hi'})
assert_match %r(/hi/hi/2$), u
end
-
+
def test_overwrite_removes_original
@params[:controller] = 'search'
@params[:action] = 'list'
@params[:list_page] = 1
-
+
assert_equal '/search/list?list_page=2', @rewriter.rewrite(:only_path => true, :overwrite_params => {"list_page" => 2})
u = @rewriter.rewrite(:only_path => false, :overwrite_params => {:list_page => 2})
assert_equal 'http://test.host/search/list?list_page=2', u
@@ -86,19 +86,19 @@ class UrlRewriterTests < Test::Unit::TestCase
end
class UrlWriterTests < Test::Unit::TestCase
-
+
class W
include ActionController::UrlWriter
end
-
+
def teardown
W.default_url_options.clear
end
-
+
def add_host!
W.default_url_options[:host] = 'www.basecamphq.com'
end
-
+
def test_exception_is_thrown_without_host
assert_raises RuntimeError do
W.new.url_for :controller => 'c', :action => 'a', :id => 'i'
@@ -110,35 +110,35 @@ class UrlWriterTests < Test::Unit::TestCase
W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => 'anchor')
)
end
-
+
def test_default_host
add_host!
assert_equal('http://www.basecamphq.com/c/a/i',
W.new.url_for(:controller => 'c', :action => 'a', :id => 'i')
)
end
-
+
def test_host_may_be_overridden
add_host!
assert_equal('http://37signals.basecamphq.com/c/a/i',
W.new.url_for(:host => '37signals.basecamphq.com', :controller => 'c', :action => 'a', :id => 'i')
)
end
-
+
def test_port
add_host!
assert_equal('http://www.basecamphq.com:3000/c/a/i',
W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :port => 3000)
)
end
-
+
def test_protocol
add_host!
assert_equal('https://www.basecamphq.com/c/a/i',
W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https')
)
end
-
+
def test_protocol_with_and_without_separator
add_host!
assert_equal('https://www.basecamphq.com/c/a/i',
@@ -184,15 +184,15 @@ class UrlWriterTests < Test::Unit::TestCase
end
def test_relative_url_root_is_respected
- orig_relative_url_root = ActionController::AbstractRequest.relative_url_root
- ActionController::AbstractRequest.relative_url_root = '/subdir'
+ orig_relative_url_root = ActionController::Base.relative_url_root
+ ActionController::Base.relative_url_root = '/subdir'
add_host!
assert_equal('https://www.basecamphq.com/subdir/c/a/i',
W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https')
)
ensure
- ActionController::AbstractRequest.relative_url_root = orig_relative_url_root
+ ActionController::Base.relative_url_root = orig_relative_url_root
end
def test_named_routes
@@ -217,8 +217,8 @@ class UrlWriterTests < Test::Unit::TestCase
end
def test_relative_url_root_is_respected_for_named_routes
- orig_relative_url_root = ActionController::AbstractRequest.relative_url_root
- ActionController::AbstractRequest.relative_url_root = '/subdir'
+ orig_relative_url_root = ActionController::Base.relative_url_root
+ ActionController::Base.relative_url_root = '/subdir'
ActionController::Routing::Routes.draw do |map|
map.home '/home/sweet/home/:user', :controller => 'home', :action => 'index'
@@ -231,7 +231,7 @@ class UrlWriterTests < Test::Unit::TestCase
controller.send(:home_url, :host => 'www.basecamphq.com', :user => 'again')
ensure
ActionController::Routing::Routes.load!
- ActionController::AbstractRequest.relative_url_root = orig_relative_url_root
+ ActionController::Base.relative_url_root = orig_relative_url_root
end
def test_only_path
@@ -239,14 +239,14 @@ class UrlWriterTests < Test::Unit::TestCase
map.home '/home/sweet/home/:user', :controller => 'home', :action => 'index'
map.connect ':controller/:action/:id'
end
-
+
# We need to create a new class in order to install the new named route.
kls = Class.new { include ActionController::UrlWriter }
controller = kls.new
assert controller.respond_to?(:home_url)
assert_equal '/brave/new/world',
controller.send(:url_for, :controller => 'brave', :action => 'new', :id => 'world', :only_path => true)
-
+
assert_equal("/home/sweet/home/alabama", controller.send(:home_url, :user => 'alabama', :host => 'unused', :only_path => true))
assert_equal("/home/sweet/home/alabama", controller.send(:home_path, 'alabama'))
ensure
@@ -306,5 +306,4 @@ class UrlWriterTests < Test::Unit::TestCase
def extract_params(url)
url.split('?', 2).last.split('&')
end
-
end
diff --git a/actionpack/test/controller/view_paths_test.rb b/actionpack/test/controller/view_paths_test.rb
index 85fa58a45b..b859a92cbd 100644
--- a/actionpack/test/controller/view_paths_test.rb
+++ b/actionpack/test/controller/view_paths_test.rb
@@ -54,10 +54,7 @@ class ViewLoadPathsTest < Test::Unit::TestCase
assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz'], @controller.view_paths
@controller.append_view_path(FIXTURE_LOAD_PATH)
- assert_equal ['foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths
-
- @controller.append_view_path([FIXTURE_LOAD_PATH])
- assert_equal ['foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths
+ assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths
end
def test_controller_prepends_view_path_correctly
@@ -68,10 +65,7 @@ class ViewLoadPathsTest < Test::Unit::TestCase
assert_equal ['foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths
@controller.prepend_view_path(FIXTURE_LOAD_PATH)
- assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz'], @controller.view_paths
-
- @controller.prepend_view_path([FIXTURE_LOAD_PATH])
- assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz'], @controller.view_paths
+ assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths
end
def test_template_appends_view_path_correctly
diff --git a/actionpack/test/fixtures/_top_level_partial.html.erb b/actionpack/test/fixtures/_top_level_partial.html.erb
new file mode 100644
index 0000000000..0b1c2e46e0
--- /dev/null
+++ b/actionpack/test/fixtures/_top_level_partial.html.erb
@@ -0,0 +1 @@
+top level partial html \ No newline at end of file
diff --git a/actionpack/test/fixtures/_top_level_partial_only.erb b/actionpack/test/fixtures/_top_level_partial_only.erb
new file mode 100644
index 0000000000..44f25b61d0
--- /dev/null
+++ b/actionpack/test/fixtures/_top_level_partial_only.erb
@@ -0,0 +1 @@
+top level partial \ No newline at end of file
diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb
new file mode 100644
index 0000000000..d7f43ad95e
--- /dev/null
+++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb
@@ -0,0 +1,3 @@
+<body>
+<% cache do %><p>ERB</p><% end %>
+</body> \ No newline at end of file
diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.js.rjs b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.js.rjs
new file mode 100644
index 0000000000..057f15e62f
--- /dev/null
+++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.js.rjs
@@ -0,0 +1,6 @@
+page.assign 'title', 'Hey'
+cache do
+ page['element_1'].visual_effect :highlight
+ page['element_2'].visual_effect :highlight
+end
+page.assign 'footer', 'Bye'
diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder
new file mode 100644
index 0000000000..efdcc28e0f
--- /dev/null
+++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder
@@ -0,0 +1,5 @@
+xml.body do
+ cache do
+ xml.p "Builder"
+ end
+end
diff --git a/actionpack/test/fixtures/test/_counter.html.erb b/actionpack/test/fixtures/test/_counter.html.erb
new file mode 100644
index 0000000000..fd245bfc70
--- /dev/null
+++ b/actionpack/test/fixtures/test/_counter.html.erb
@@ -0,0 +1 @@
+<%= counter_counter %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_layout_for_block_with_args.html.erb b/actionpack/test/fixtures/test/_layout_for_block_with_args.html.erb
new file mode 100644
index 0000000000..307533208d
--- /dev/null
+++ b/actionpack/test/fixtures/test/_layout_for_block_with_args.html.erb
@@ -0,0 +1,3 @@
+Before
+<%= yield 'arg1', 'arg2' %>
+After \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/implicit_content_type.atom.builder b/actionpack/test/fixtures/test/implicit_content_type.atom.builder
new file mode 100644
index 0000000000..2fcb32d247
--- /dev/null
+++ b/actionpack/test/fixtures/test/implicit_content_type.atom.builder
@@ -0,0 +1,2 @@
+xml.atom do
+end
diff --git a/actionpack/test/fixtures/test/using_layout_around_block_with_args.html.erb b/actionpack/test/fixtures/test/using_layout_around_block_with_args.html.erb
new file mode 100644
index 0000000000..71b1f30ad0
--- /dev/null
+++ b/actionpack/test/fixtures/test/using_layout_around_block_with_args.html.erb
@@ -0,0 +1 @@
+<% render(:layout => "layout_for_block_with_args") do |*args| %><%= args.join %><% end %> \ No newline at end of file
diff --git a/actionpack/test/template/active_record_helper_i18n_test.rb b/actionpack/test/template/active_record_helper_i18n_test.rb
new file mode 100644
index 0000000000..7ba9659814
--- /dev/null
+++ b/actionpack/test/template/active_record_helper_i18n_test.rb
@@ -0,0 +1,46 @@
+require 'abstract_unit'
+
+class ActiveRecordHelperI18nTest < Test::Unit::TestCase
+ include ActionView::Helpers::ActiveRecordHelper
+
+ attr_reader :request
+ uses_mocha 'active_record_helper_i18n_test' do
+ def setup
+ @object = stub :errors => stub(:count => 1, :full_messages => ['full_messages'])
+ @object_name = 'book'
+ stubs(:content_tag).returns 'content_tag'
+
+ I18n.stubs(:t).with(:'header', :locale => 'en-US', :scope => [:activerecord, :errors, :template], :count => 1, :model => '').returns "1 error prohibited this from being saved"
+ I18n.stubs(:t).with(:'body', :locale => 'en-US', :scope => [:activerecord, :errors, :template]).returns 'There were problems with the following fields:'
+ end
+
+ def test_error_messages_for_given_a_header_option_it_does_not_translate_header_message
+ I18n.expects(:translate).with(:'header', :locale => 'en-US', :scope => [:activerecord, :errors, :template], :count => 1, :model => '').never
+ error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en-US')
+ end
+
+ def test_error_messages_for_given_no_header_option_it_translates_header_message
+ I18n.expects(:t).with(:'header', :locale => 'en-US', :scope => [:activerecord, :errors, :template], :count => 1, :model => '').returns 'header message'
+ I18n.expects(:t).with('', :default => '', :count => 1, :scope => [:activerecord, :models]).once.returns ''
+ error_messages_for(:object => @object, :locale => 'en-US')
+ end
+
+ def test_error_messages_for_given_a_message_option_it_does_not_translate_message
+ I18n.expects(:t).with(:'body', :locale => 'en-US', :scope => [:activerecord, :errors, :template]).never
+ I18n.expects(:t).with('', :default => '', :count => 1, :scope => [:activerecord, :models]).once.returns ''
+ error_messages_for(:object => @object, :message => 'message', :locale => 'en-US')
+ end
+
+ def test_error_messages_for_given_no_message_option_it_translates_message
+ I18n.expects(:t).with(:'body', :locale => 'en-US', :scope => [:activerecord, :errors, :template]).returns 'There were problems with the following fields:'
+ I18n.expects(:t).with('', :default => '', :count => 1, :scope => [:activerecord, :models]).once.returns ''
+ error_messages_for(:object => @object, :locale => 'en-US')
+ end
+
+ def test_error_messages_for_given_object_name_it_translates_object_name
+ I18n.expects(:t).with(:header, :locale => 'en-US', :scope => [:activerecord, :errors, :template], :count => 1, :model => @object_name).returns "1 error prohibited this #{@object_name} from being saved"
+ I18n.expects(:t).with(@object_name, :default => @object_name, :count => 1, :scope => [:activerecord, :models]).once.returns @object_name
+ error_messages_for(:object => @object, :locale => 'en-US', :object_name => @object_name)
+ end
+ end
+end
diff --git a/actionpack/test/template/active_record_helper_test.rb b/actionpack/test/template/active_record_helper_test.rb
index dfc30e651a..e46f95d18b 100644
--- a/actionpack/test/template/active_record_helper_test.rb
+++ b/actionpack/test/template/active_record_helper_test.rb
@@ -10,17 +10,17 @@ class ActiveRecordHelperTest < ActionView::TestCase
alias_method :body_before_type_cast, :body unless respond_to?(:body_before_type_cast)
alias_method :author_name_before_type_cast, :author_name unless respond_to?(:author_name_before_type_cast)
end
-
+
User = Struct.new("User", :email)
User.class_eval do
alias_method :email_before_type_cast, :email unless respond_to?(:email_before_type_cast)
end
-
+
Column = Struct.new("Column", :type, :name, :human_name)
end
def setup_post
- @post = Post.new
+ @post = Post.new
def @post.errors
Class.new {
def on(field)
@@ -33,12 +33,12 @@ class ActiveRecordHelperTest < ActionView::TestCase
false
end
end
- def empty?() false end
- def count() 1 end
+ def empty?() false end
+ def count() 1 end
def full_messages() [ "Author name can't be empty" ] end
}.new
end
-
+
def @post.new_record?() true end
def @post.to_param() nil end
@@ -58,16 +58,16 @@ class ActiveRecordHelperTest < ActionView::TestCase
end
def setup_user
- @user = User.new
+ @user = User.new
def @user.errors
Class.new {
def on(field) field == "email" end
- def empty?() false end
- def count() 1 end
+ def empty?() false end
+ def count() 1 end
def full_messages() [ "User email can't be empty" ] end
}.new
end
-
+
def @user.new_record?() true end
def @user.to_param() nil end
@@ -81,7 +81,7 @@ class ActiveRecordHelperTest < ActionView::TestCase
@user.email = ""
end
-
+
def protect_against_forgery?
@protect_against_forgery ? true : false
end
@@ -92,7 +92,7 @@ class ActiveRecordHelperTest < ActionView::TestCase
setup_user
@response = ActionController::TestResponse.new
-
+
@controller = Object.new
def @controller.url_for(options)
options = options.symbolize_keys
@@ -111,7 +111,7 @@ class ActiveRecordHelperTest < ActionView::TestCase
assert_dom_equal(
%(<div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div>),
text_area("post", "body")
- )
+ )
end
def test_text_field_with_errors
@@ -140,7 +140,7 @@ class ActiveRecordHelperTest < ActionView::TestCase
form("post")
)
end
-
+
def test_form_with_protect_against_forgery
@protect_against_forgery = true
@request_forgery_protection_token = 'authenticity_token'
@@ -150,7 +150,7 @@ class ActiveRecordHelperTest < ActionView::TestCase
form("post")
)
end
-
+
def test_form_with_method_option
assert_dom_equal(
%(<form action="create" method="get"><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Create" /></form>),
@@ -211,9 +211,9 @@ class ActiveRecordHelperTest < ActionView::TestCase
other_post = @post
assert_dom_equal "<div class=\"formError\">can't be empty</div>", error_message_on(other_post, :author_name)
end
-
- def test_error_message_on_should_use_options
- assert_dom_equal "<div class=\"differentError\">beforecan't be emptyafter</div>", error_message_on(:post, :author_name, "before", "after", "differentError")
+
+ def test_error_message_on_with_options_hash
+ assert_dom_equal "<div class=\"differentError\">beforecan't be emptyafter</div>", error_message_on(:post, :author_name, :css_class => 'differentError', :prepend_text => 'before', :append_text => 'after')
end
def test_error_messages_for_many_objects
@@ -224,10 +224,10 @@ class ActiveRecordHelperTest < ActionView::TestCase
# add the default to put post back in the title
assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for("user", "post", :object_name => "post")
-
+
# symbols work as well
assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :object_name => :post)
-
+
# any default works too
assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this monkey from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :object_name => "monkey")
@@ -242,7 +242,7 @@ class ActiveRecordHelperTest < ActionView::TestCase
message = "Please fix the following fields and resubmit:"
assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>#{header_message}</h2><p>#{message}</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :header_message => header_message, :message => message)
end
-
+
def test_error_messages_for_non_instance_variable
actual_user = @user
actual_post = @post
@@ -251,14 +251,14 @@ class ActiveRecordHelperTest < ActionView::TestCase
#explicitly set object
assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post", :object => actual_post)
-
+
#multiple objects
assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this user from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for("user", "post", :object => [actual_user, actual_post])
-
+
#nil object
assert_equal '', error_messages_for('user', :object => nil)
end
-
+
def test_form_with_string_multipart
assert_dom_equal(
%(<form action="create" enctype="multipart/form-data" method="post"><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Create" /></form>),
diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb
index 020e112fd0..7e40a55dc5 100644
--- a/actionpack/test/template/asset_tag_helper_test.rb
+++ b/actionpack/test/template/asset_tag_helper_test.rb
@@ -30,7 +30,6 @@ class AssetTagHelperTest < ActionView::TestCase
end.new
@request = Class.new do
- def relative_url_root() "" end
def protocol() 'http://' end
def ssl?() false end
def host_with_port() 'localhost' end
@@ -39,8 +38,7 @@ class AssetTagHelperTest < ActionView::TestCase
@controller.request = @request
ActionView::Helpers::AssetTagHelper::reset_javascript_include_default
-
- ActionView::Base.computed_public_paths.clear
+ COMPUTED_PUBLIC_PATHS.clear
end
def teardown
@@ -119,7 +117,7 @@ class AssetTagHelperTest < ActionView::TestCase
%(image_path("xml")) => %(/images/xml),
%(image_path("xml.png")) => %(/images/xml.png),
%(image_path("dir/xml.png")) => %(/images/dir/xml.png),
- %(image_path("/dir/xml.png")) => %(/dir/xml.png)
+ %(image_path("/dir/xml.png")) => %(/dir/xml.png)
}
PathToImageToTag = {
@@ -161,7 +159,7 @@ class AssetTagHelperTest < ActionView::TestCase
ENV["RAILS_ASSET_ID"] = ""
JavascriptIncludeToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- ActionView::Base.computed_public_paths.clear
+ COMPUTED_PUBLIC_PATHS.clear
ENV["RAILS_ASSET_ID"] = "1"
assert_dom_equal(%(<script src="/javascripts/prototype.js?1" type="text/javascript"></script>\n<script src="/javascripts/effects.js?1" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js?1" type="text/javascript"></script>\n<script src="/javascripts/controls.js?1" type="text/javascript"></script>\n<script src="/javascripts/application.js?1" type="text/javascript"></script>), javascript_include_tag(:defaults))
@@ -174,7 +172,7 @@ class AssetTagHelperTest < ActionView::TestCase
ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'lib1', '/elsewhere/blub/lib2'
assert_dom_equal %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/slider.js" type="text/javascript"></script>\n<script src="/javascripts/lib1.js" type="text/javascript"></script>\n<script src="/elsewhere/blub/lib2.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), javascript_include_tag(:defaults)
end
-
+
def test_custom_javascript_expansions
ActionView::Helpers::AssetTagHelper::register_javascript_expansion :monkey => ["head", "body", "tail"]
assert_dom_equal %(<script src="/javascripts/first.js" type="text/javascript"></script>\n<script src="/javascripts/head.js" type="text/javascript"></script>\n<script src="/javascripts/body.js" type="text/javascript"></script>\n<script src="/javascripts/tail.js" type="text/javascript"></script>\n<script src="/javascripts/last.js" type="text/javascript"></script>), javascript_include_tag('first', :monkey, 'last')
@@ -217,7 +215,7 @@ class AssetTagHelperTest < ActionView::TestCase
def test_image_path
ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
-
+
def test_path_to_image_alias_for_image_path
PathToImageToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
@@ -225,7 +223,7 @@ class AssetTagHelperTest < ActionView::TestCase
def test_image_tag
ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
-
+
def test_timebased_asset_id
expected_time = File.stat(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).mtime.to_i.to_s
assert_equal %(<img alt="Rails" src="/images/rails.png?#{expected_time}" />), image_tag("rails.png")
@@ -234,7 +232,7 @@ class AssetTagHelperTest < ActionView::TestCase
def test_should_skip_asset_id_on_complete_url
assert_equal %(<img alt="Rails" src="http://www.example.com/rails.png" />), image_tag("http://www.example.com/rails.png")
end
-
+
def test_should_use_preset_asset_id
ENV["RAILS_ASSET_ID"] = "4500"
assert_equal %(<img alt="Rails" src="/images/rails.png?4500" />), image_tag("rails.png")
@@ -256,14 +254,14 @@ class AssetTagHelperTest < ActionView::TestCase
ENV["RAILS_ASSET_ID"] = ""
ActionController::Base.asset_host = 'http://a0.example.com'
ActionController::Base.perform_caching = true
-
+
assert_dom_equal(
%(<script src="http://a0.example.com/javascripts/all.js" type="text/javascript"></script>),
javascript_include_tag(:all, :cache => true)
)
assert File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js'))
-
+
assert_dom_equal(
%(<script src="http://a0.example.com/javascripts/money.js" type="text/javascript"></script>),
javascript_include_tag(:all, :cache => "money")
@@ -335,8 +333,9 @@ class AssetTagHelperTest < ActionView::TestCase
ActionController::Base.asset_host = 'http://a%d.example.com'
ActionController::Base.perform_caching = true
+ hash = '/javascripts/cache/money.js'.hash % 4
assert_dom_equal(
- %(<script src="http://a3.example.com/javascripts/cache/money.js" type="text/javascript"></script>),
+ %(<script src="http://a#{hash}.example.com/javascripts/cache/money.js" type="text/javascript"></script>),
javascript_include_tag(:all, :cache => "cache/money")
)
@@ -344,7 +343,7 @@ class AssetTagHelperTest < ActionView::TestCase
ensure
FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'cache', 'money.js'))
end
-
+
def test_caching_javascript_include_tag_with_all_and_recursive_puts_defaults_at_the_start_of_the_file
ENV["RAILS_ASSET_ID"] = ""
ActionController::Base.asset_host = 'http://a0.example.com'
@@ -390,7 +389,7 @@ class AssetTagHelperTest < ActionView::TestCase
def test_caching_javascript_include_tag_when_caching_off
ENV["RAILS_ASSET_ID"] = ""
ActionController::Base.perform_caching = false
-
+
assert_dom_equal(
%(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>),
javascript_include_tag(:all, :cache => true)
@@ -402,7 +401,7 @@ class AssetTagHelperTest < ActionView::TestCase
)
assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js'))
-
+
assert_dom_equal(
%(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>),
javascript_include_tag(:all, :cache => "money")
@@ -420,13 +419,14 @@ class AssetTagHelperTest < ActionView::TestCase
ENV["RAILS_ASSET_ID"] = ""
ActionController::Base.asset_host = 'http://a0.example.com'
ActionController::Base.perform_caching = true
-
+
assert_dom_equal(
%(<link href="http://a0.example.com/stylesheets/all.css" media="screen" rel="stylesheet" type="text/css" />),
stylesheet_link_tag(:all, :cache => true)
)
- assert File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css'))
+ expected = Dir["#{ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR}/*.css"].map { |p| File.mtime(p) }.max
+ assert_equal expected, File.mtime(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css'))
assert_dom_equal(
%(<link href="http://a0.example.com/stylesheets/money.css" media="screen" rel="stylesheet" type="text/css" />),
@@ -459,7 +459,7 @@ class AssetTagHelperTest < ActionView::TestCase
def test_caching_stylesheet_include_tag_when_caching_off
ENV["RAILS_ASSET_ID"] = ""
ActionController::Base.perform_caching = false
-
+
assert_dom_equal(
%(<link href="/stylesheets/bank.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/robber.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/version.1.0.css" media="screen" rel="stylesheet" type="text/css" />),
stylesheet_link_tag(:all, :cache => true)
@@ -471,7 +471,7 @@ class AssetTagHelperTest < ActionView::TestCase
)
assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css'))
-
+
assert_dom_equal(
%(<link href="/stylesheets/bank.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/robber.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/version.1.0.css" media="screen" rel="stylesheet" type="text/css" />),
stylesheet_link_tag(:all, :cache => "money")
@@ -490,6 +490,8 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase
tests ActionView::Helpers::AssetTagHelper
def setup
+ ActionController::Base.relative_url_root = "/collaboration/hieraki"
+
@controller = Class.new do
attr_accessor :request
@@ -497,22 +499,22 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase
"http://www.example.com/collaboration/hieraki"
end
end.new
-
- @request = Class.new do
- def relative_url_root
- "/collaboration/hieraki"
- end
+ @request = Class.new do
def protocol
'gopher://'
end
end.new
-
+
@controller.request = @request
-
+
ActionView::Helpers::AssetTagHelper::reset_javascript_include_default
end
+ def teardown
+ ActionController::Base.relative_url_root = nil
+ end
+
def test_should_compute_proper_path
assert_dom_equal(%(<link href="http://www.example.com/collaboration/hieraki" rel="alternate" title="RSS" type="application/rss+xml" />), auto_discovery_link_tag)
assert_dom_equal(%(/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr"))
@@ -521,7 +523,7 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase
assert_dom_equal(%(<img alt="Mouse" onmouseover="this.src='/collaboration/hieraki/images/mouse_over.png'" onmouseout="this.src='/collaboration/hieraki/images/mouse.png'" src="/collaboration/hieraki/images/mouse.png" />), image_tag("mouse.png", :mouseover => "/images/mouse_over.png"))
assert_dom_equal(%(<img alt="Mouse2" onmouseover="this.src='/collaboration/hieraki/images/mouse_over2.png'" onmouseout="this.src='/collaboration/hieraki/images/mouse2.png'" src="/collaboration/hieraki/images/mouse2.png" />), image_tag("mouse2.png", :mouseover => image_path("mouse_over2.png")))
end
-
+
def test_should_ignore_relative_root_path_on_complete_url
assert_dom_equal(%(http://www.example.com/images/xml.png), image_path("http://www.example.com/images/xml.png"))
end
diff --git a/actionpack/test/template/atom_feed_helper_test.rb b/actionpack/test/template/atom_feed_helper_test.rb
index 9f7e5b4c6c..ef31ab2c76 100644
--- a/actionpack/test/template/atom_feed_helper_test.rb
+++ b/actionpack/test/template/atom_feed_helper_test.rb
@@ -74,12 +74,30 @@ class ScrollsController < ActionController::Base
end
end
EOT
+ FEEDS["feed_with_overridden_ids"] = <<-EOT
+ atom_feed({:id => 'tag:test.rubyonrails.org,2008:test/'}) do |feed|
+ feed.title("My great blog!")
+ feed.updated((@scrolls.first.created_at))
+
+ for scroll in @scrolls
+ feed.entry(scroll, :id => "tag:test.rubyonrails.org,2008:"+scroll.id.to_s) do |entry|
+ entry.title(scroll.title)
+ entry.content(scroll.body, :type => 'html')
+ entry.tag!('app:edited', Time.now)
+
+ entry.author do |author|
+ author.name("DHH")
+ end
+ end
+ end
+ end
+ EOT
def index
@scrolls = [
Scroll.new(1, "1", "Hello One", "Something <i>COOL!</i>", Time.utc(2007, 12, 12, 15), Time.utc(2007, 12, 12, 15)),
Scroll.new(2, "2", "Hello Two", "Something Boring", Time.utc(2007, 12, 12, 15)),
]
-
+
render :inline => FEEDS[params[:id]], :type => :builder
end
@@ -98,21 +116,21 @@ class AtomFeedTest < Test::Unit::TestCase
@request.host = "www.nextangle.com"
end
-
+
def test_feed_should_use_default_language_if_none_is_given
with_restful_routing(:scrolls) do
get :index, :id => "defaults"
assert_match %r{xml:lang="en-US"}, @response.body
end
end
-
+
def test_feed_should_include_two_entries
with_restful_routing(:scrolls) do
get :index, :id => "defaults"
assert_select "entry", 2
end
end
-
+
def test_entry_should_only_use_published_if_created_at_is_present
with_restful_routing(:scrolls) do
get :index, :id => "defaults"
@@ -167,7 +185,16 @@ class AtomFeedTest < Test::Unit::TestCase
end
end
- private
+ def test_feed_should_allow_overriding_ids
+ with_restful_routing(:scrolls) do
+ get :index, :id => "feed_with_overridden_ids"
+ assert_select "id", :text => "tag:test.rubyonrails.org,2008:test/"
+ assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:1"
+ assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:2"
+ end
+ end
+
+private
def with_restful_routing(resources)
with_routing do |set|
set.draw do |map|
diff --git a/actionpack/test/template/compiled_templates_test.rb b/actionpack/test/template/compiled_templates_test.rb
new file mode 100644
index 0000000000..e005aa0f03
--- /dev/null
+++ b/actionpack/test/template/compiled_templates_test.rb
@@ -0,0 +1,47 @@
+require 'abstract_unit'
+require 'controller/fake_models'
+
+uses_mocha 'TestTemplateRecompilation' do
+ class CompiledTemplatesTest < Test::Unit::TestCase
+ def setup
+ @compiled_templates = ActionView::Base::CompiledTemplates
+ @compiled_templates.instance_methods.each do |m|
+ @compiled_templates.send(:remove_method, m) if m =~ /^_run_/
+ end
+ end
+
+ def test_template_gets_compiled
+ assert_equal 0, @compiled_templates.instance_methods.size
+ assert_equal "Hello world!", render("test/hello_world.erb")
+ assert_equal 1, @compiled_templates.instance_methods.size
+ end
+
+ def test_template_gets_recompiled_when_using_different_keys_in_local_assigns
+ assert_equal 0, @compiled_templates.instance_methods.size
+ assert_equal "Hello world!", render("test/hello_world.erb")
+ assert_equal "Hello world!", render("test/hello_world.erb", {:foo => "bar"})
+ assert_equal 2, @compiled_templates.instance_methods.size
+ end
+
+ def test_compiled_template_will_not_be_recompiled_when_rendered_with_identical_local_assigns
+ assert_equal 0, @compiled_templates.instance_methods.size
+ assert_equal "Hello world!", render("test/hello_world.erb")
+ ActionView::Template.any_instance.expects(:compile!).never
+ assert_equal "Hello world!", render("test/hello_world.erb")
+ end
+
+ def test_compiled_template_will_always_be_recompiled_when_eager_loaded_templates_is_off
+ ActionView::PathSet::Path.expects(:eager_load_templates?).times(4).returns(false)
+ assert_equal 0, @compiled_templates.instance_methods.size
+ assert_equal "Hello world!", render("#{FIXTURE_LOAD_PATH}/test/hello_world.erb")
+ ActionView::Template.any_instance.expects(:compile!).times(3)
+ 3.times { assert_equal "Hello world!", render("#{FIXTURE_LOAD_PATH}/test/hello_world.erb") }
+ assert_equal 1, @compiled_templates.instance_methods.size
+ end
+
+ private
+ def render(*args)
+ ActionView::Base.new(ActionController::Base.view_paths, {}).render(*args)
+ end
+ end
+end
diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb
new file mode 100644
index 0000000000..bf3b2588c8
--- /dev/null
+++ b/actionpack/test/template/date_helper_i18n_test.rb
@@ -0,0 +1,113 @@
+require 'abstract_unit'
+
+class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase
+ include ActionView::Helpers::DateHelper
+ attr_reader :request
+
+ def setup
+ @from = Time.mktime(2004, 6, 6, 21, 45, 0)
+ end
+
+ uses_mocha 'date_helper_distance_of_time_in_words_i18n_test' do
+ # distance_of_time_in_words
+
+ def test_distance_of_time_in_words_calls_i18n
+ { # with include_seconds
+ [2.seconds, true] => [:'less_than_x_seconds', 5],
+ [9.seconds, true] => [:'less_than_x_seconds', 10],
+ [19.seconds, true] => [:'less_than_x_seconds', 20],
+ [30.seconds, true] => [:'half_a_minute', nil],
+ [59.seconds, true] => [:'less_than_x_minutes', 1],
+ [60.seconds, true] => [:'x_minutes', 1],
+
+ # without include_seconds
+ [29.seconds, false] => [:'less_than_x_minutes', 1],
+ [60.seconds, false] => [:'x_minutes', 1],
+ [44.minutes, false] => [:'x_minutes', 44],
+ [61.minutes, false] => [:'about_x_hours', 1],
+ [24.hours, false] => [:'x_days', 1],
+ [30.days, false] => [:'about_x_months', 1],
+ [60.days, false] => [:'x_months', 2],
+ [1.year, false] => [:'about_x_years', 1],
+ [3.years, false] => [:'over_x_years', 3]
+
+ }.each do |passed, expected|
+ assert_distance_of_time_in_words_translates_key passed, expected
+ end
+ end
+
+ def assert_distance_of_time_in_words_translates_key(passed, expected)
+ diff, include_seconds = *passed
+ key, count = *expected
+ to = @from + diff
+
+ options = {:locale => 'en-US', :scope => :'datetime.distance_in_words'}
+ options[:count] = count if count
+
+ I18n.expects(:t).with(key, options)
+ distance_of_time_in_words(@from, to, include_seconds, :locale => 'en-US')
+ end
+
+ def test_distance_of_time_pluralizations
+ { [:'less_than_x_seconds', 1] => 'less than 1 second',
+ [:'less_than_x_seconds', 2] => 'less than 2 seconds',
+ [:'less_than_x_minutes', 1] => 'less than a minute',
+ [:'less_than_x_minutes', 2] => 'less than 2 minutes',
+ [:'x_minutes', 1] => '1 minute',
+ [:'x_minutes', 2] => '2 minutes',
+ [:'about_x_hours', 1] => 'about 1 hour',
+ [:'about_x_hours', 2] => 'about 2 hours',
+ [:'x_days', 1] => '1 day',
+ [:'x_days', 2] => '2 days',
+ [:'about_x_years', 1] => 'about 1 year',
+ [:'about_x_years', 2] => 'about 2 years',
+ [:'over_x_years', 1] => 'over 1 year',
+ [:'over_x_years', 2] => 'over 2 years'
+
+ }.each do |args, expected|
+ key, count = *args
+ assert_equal expected, I18n.t(key, :count => count, :scope => 'datetime.distance_in_words')
+ end
+ end
+ end
+end
+
+class DateHelperSelectTagsI18nTests < Test::Unit::TestCase
+ include ActionView::Helpers::DateHelper
+ attr_reader :request
+
+ uses_mocha 'date_helper_select_tags_i18n_tests' do
+ def setup
+ I18n.stubs(:translate).with(:'date.month_names', :locale => 'en-US').returns Date::MONTHNAMES
+ end
+
+ # select_month
+
+ def test_select_month_given_use_month_names_option_does_not_translate_monthnames
+ I18n.expects(:translate).never
+ select_month(8, :locale => 'en-US', :use_month_names => Date::MONTHNAMES)
+ end
+
+ def test_select_month_translates_monthnames
+ I18n.expects(:translate).with(:'date.month_names', :locale => 'en-US').returns Date::MONTHNAMES
+ select_month(8, :locale => 'en-US')
+ end
+
+ def test_select_month_given_use_short_month_option_translates_abbr_monthnames
+ I18n.expects(:translate).with(:'date.abbr_month_names', :locale => 'en-US').returns Date::ABBR_MONTHNAMES
+ select_month(8, :locale => 'en-US', :use_short_month => true)
+ end
+
+ # date_or_time_select
+
+ def test_date_or_time_select_given_an_order_options_does_not_translate_order
+ I18n.expects(:translate).never
+ datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en-US')
+ end
+
+ def test_date_or_time_select_given_no_order_options_translates_order
+ I18n.expects(:translate).with(:'date.order', :locale => 'en-US').returns [:year, :month, :day]
+ datetime_select('post', 'updated_at', :locale => 'en-US')
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb
index 8b4e94c67f..1a645bccc6 100755..100644
--- a/actionpack/test/template/date_helper_test.rb
+++ b/actionpack/test/template/date_helper_test.rb
@@ -17,7 +17,7 @@ class DateHelperTest < ActionView::TestCase
end
end
end
-
+
def assert_distance_of_time_in_words(from, to=nil)
to ||= from
@@ -86,13 +86,13 @@ class DateHelperTest < ActionView::TestCase
from = Time.mktime(2004, 6, 6, 21, 45, 0)
assert_distance_of_time_in_words(from)
end
-
+
def test_distance_in_words_with_time_zones
from = Time.mktime(2004, 6, 6, 21, 45, 0)
assert_distance_of_time_in_words(from.in_time_zone('Alaska'))
assert_distance_of_time_in_words(from.in_time_zone('Hawaii'))
end
-
+
def test_distance_in_words_with_different_time_zones
from = Time.mktime(2004, 6, 6, 21, 45, 0)
assert_distance_of_time_in_words(
@@ -100,13 +100,13 @@ class DateHelperTest < ActionView::TestCase
from.in_time_zone('Hawaii')
)
end
-
+
def test_distance_in_words_with_dates
start_date = Date.new 1975, 1, 31
end_date = Date.new 1977, 1, 31
assert_equal("over 2 years", distance_of_time_in_words(start_date, end_date))
end
-
+
def test_distance_in_words_with_integers
assert_equal "less than a minute", distance_of_time_in_words(59)
assert_equal "about 1 hour", distance_of_time_in_words(60*60)
@@ -557,11 +557,8 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_date_with_incomplete_order
- expected = %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_year" name="date[first][year]">\n)
+ # NOTE: modified this test because of minimal API change
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
@@ -569,6 +566,10 @@ class DateHelperTest < ActionView::TestCase
expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
expected << "</select>\n"
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:day])
end
@@ -757,6 +758,26 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => "selector")
end
+ def test_select_date_with_separator
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"})
+ end
+
def test_select_datetime
expected = %(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
@@ -857,6 +878,42 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector')
end
+ def test_select_datetime_with_all_separators
+ expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << "/"
+
+ expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << "/"
+
+ expected << %(<select id="date_first_day" name="date[first][day]" class="selector">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << "&mdash;"
+
+ expected << %(<select id="date_first_hour" name="date[first][hour]" class="selector">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << ":"
+
+ expected << %(<select id="date_first_minute" name="date[first][minute]" class="selector">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), { :datetime_separator => "&mdash;", :date_separator => "/", :time_separator => ":", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector')
+ end
+
+ def test_select_datetime_should_work_with_date
+ assert_nothing_raised { select_datetime(Date.today) }
+ end
+
def test_select_time
expected = %(<select id="date_hour" name="date[hour]">\n)
expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
@@ -933,32 +990,9 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector')
assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {:include_seconds => false}, :class => 'selector')
end
-
- uses_mocha 'TestDatetimeAndTimeSelectUseTimeCurrentAsDefault' do
- def test_select_datetime_uses_time_current_as_default
- time = stub(:year => 2004, :month => 6, :day => 15, :hour => 16, :min => 35, :sec => 0)
- Time.expects(:current).returns time
- expects(:select_date).with(time, anything, anything).returns('')
- expects(:select_time).with(time, anything, anything).returns('')
- select_datetime
- end
-
- def test_select_time_uses_time_current_as_default
- time = stub(:year => 2004, :month => 6, :day => 15, :hour => 16, :min => 35, :sec => 0)
- Time.expects(:current).returns time
- expects(:select_hour).with(time, anything, anything).returns('')
- expects(:select_minute).with(time, anything, anything).returns('')
- select_time
- end
-
- def test_select_date_uses_date_current_as_default
- date = stub(:year => 2004, :month => 6, :day => 15)
- Date.expects(:current).returns date
- expects(:select_year).with(date, anything, anything).returns('')
- expects(:select_month).with(date, anything, anything).returns('')
- expects(:select_day).with(date, anything, anything).returns('')
- select_date
- end
+
+ def test_select_time_should_work_with_date
+ assert_nothing_raised { select_time(Date.today) }
end
def test_date_select
@@ -1179,6 +1213,30 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_date_select_with_separator
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", { :date_separator => " / " })
+ end
+
def test_time_select
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
@@ -1188,11 +1246,11 @@ class DateHelperTest < ActionView::TestCase
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, time_select("post", "written_on")
@@ -1203,11 +1261,11 @@ class DateHelperTest < ActionView::TestCase
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
expected = %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, time_select("post", "written_on", :ignore_date => true)
@@ -1222,15 +1280,15 @@ class DateHelperTest < ActionView::TestCase
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %(<select id="post_written_on_6i" name="post[written_on(6i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 35}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, time_select("post", "written_on", :include_seconds => true)
@@ -1245,11 +1303,11 @@ class DateHelperTest < ActionView::TestCase
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="selector">\n)
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="selector">\n)
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, time_select("post", "written_on", {}, :class => 'selector')
@@ -1268,16 +1326,43 @@ class DateHelperTest < ActionView::TestCase
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="selector">\n)
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="selector">\n)
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, output_buffer
end
+ def test_time_select_with_separator
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " - "
+
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " - "
+
+ expected << %(<select id="post_written_on_6i" name="post[written_on(6i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", { :time_separator => " - ", :include_seconds => true })
+ end
+
def test_datetime_select
@post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 16, 35)
@@ -1306,7 +1391,7 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, datetime_select("post", "updated_at")
end
-
+
uses_mocha 'TestDatetimeSelectDefaultsToTimeZoneNowWhenConfigTimeZoneIsSet' do
def test_datetime_select_defaults_to_time_zone_now_when_config_time_zone_is_set
time = stub(:year => 2004, :month => 6, :day => 15, :hour => 16, :min => 35, :sec => 0)
@@ -1360,6 +1445,47 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_datetime_select_with_separators
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " , "
+
+ expected << %(<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " - "
+
+ expected << %(<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " - "
+
+ expected << %(<select id="post_updated_at_6i" name="post[updated_at(6i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", { :date_separator => " / ", :datetime_separator => " , ", :time_separator => " - ", :include_seconds => true })
+ end
+
def test_date_select_with_zero_value_and_no_start_year
expected = %(<select id="date_first_year" name="date[first][year]">\n)
(Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
@@ -1370,8 +1496,7 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected <<
-%(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
assert_dom_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]")
@@ -1388,8 +1513,7 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected <<
-%(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
assert_dom_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]")
@@ -1405,8 +1529,7 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected <<
-%(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
assert_dom_equal expected, select_date(0, :prefix => "date[first]")
@@ -1422,8 +1545,7 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected <<
-%(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
expected << "</select>\n"
assert_dom_equal expected, select_date(nil, :prefix => "date[first]")
@@ -1530,15 +1652,15 @@ class DateHelperTest < ActionView::TestCase
expected << " &mdash; "
expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %{<select id="post_updated_at_6i" name="post[updated_at(6i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 35}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, datetime_select("post", "updated_at", :include_seconds => true)
@@ -1559,11 +1681,11 @@ class DateHelperTest < ActionView::TestCase
expected << " &mdash; "
expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true)
@@ -1582,11 +1704,11 @@ class DateHelperTest < ActionView::TestCase
expected << " &mdash; "
expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, datetime_select("post", "updated_at", :discard_month => true)
@@ -1601,11 +1723,11 @@ class DateHelperTest < ActionView::TestCase
expected << %{<input type="hidden" id="post_updated_at_3i" name="post[updated_at(3i)]" value="15" />\n}
expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true, :discard_month => true)
@@ -1628,11 +1750,11 @@ class DateHelperTest < ActionView::TestCase
expected << " &mdash; "
expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:minute, :day, :hour, :month, :year, :second])
@@ -1653,11 +1775,11 @@ class DateHelperTest < ActionView::TestCase
expected << " &mdash; "
expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:day, :month])
@@ -1680,11 +1802,11 @@ class DateHelperTest < ActionView::TestCase
expected << " &mdash; "
expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, datetime_select("post", "updated_at", :default => Time.local(2006, 9, 19, 15, 16, 35))
@@ -1727,11 +1849,11 @@ class DateHelperTest < ActionView::TestCase
expected << " &mdash; "
expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 9}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 9}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 42}>#{leading_zero_on_single_digits(i)}</option>\n) }
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 42}>#{sprintf("%02d", i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, datetime_select("post", "updated_at", :default => { :month => 10, :minute => 42, :hour => 9 })
@@ -1766,33 +1888,158 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, datetime_select("post", "updated_at", {}, :class => 'selector')
end
- uses_mocha 'TestInstanceTagDefaultTimeFromOptions' do
- def test_instance_tag_default_time_from_options_uses_time_current_as_default_when_hash_passed_as_arg
- dummy_instance_tag = ActionView::Helpers::InstanceTag.new(1,2,3)
- Time.expects(:current).returns Time.now
- dummy_instance_tag.send!(:default_time_from_options, :hour => 2)
- end
-
- def test_instance_tag_default_time_from_options_respects_hash_arg_settings_when_time_falls_in_system_local_dst_spring_gap
- with_env_tz('US/Central') do
- dummy_instance_tag = ActionView::Helpers::InstanceTag.new(1,2,3)
- Time.stubs(:now).returns Time.local(2006, 4, 2, 1)
- assert_equal 2, dummy_instance_tag.send!(:default_time_from_options, :hour => 2).hour
- end
- end
-
- def test_instance_tag_default_time_from_options_handles_far_future_date
- dummy_instance_tag = ActionView::Helpers::InstanceTag.new(1,2,3)
- time = dummy_instance_tag.send!(:default_time_from_options, :year => 2050, :month => 2, :day => 10, :hour => 15, :min => 30, :sec => 45)
- assert_equal 2050, time.year
- end
+ def test_date_select_should_not_change_passed_options_hash
+ @post = Post.new
+ @post.updated_at = Time.local(2008, 7, 16, 23, 30)
+
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ date_select(@post, :updated_at, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
+ end
+
+ def test_datetime_select_should_not_change_passed_options_hash
+ @post = Post.new
+ @post.updated_at = Time.local(2008, 7, 16, 23, 30)
+
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ datetime_select(@post, :updated_at, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
+ end
+
+ def test_time_select_should_not_change_passed_options_hash
+ @post = Post.new
+ @post.updated_at = Time.local(2008, 7, 16, 23, 30)
+
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ time_select(@post, :updated_at, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
+ end
+
+ def test_select_date_should_not_change_passed_options_hash
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ select_date(Date.today, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
+ end
+
+ def test_select_datetime_should_not_change_passed_options_hash
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ select_datetime(Time.now, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
+ end
+
+ def test_select_time_should_not_change_passed_options_hash
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ select_time(Time.now, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
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
end
diff --git a/actionpack/test/template/form_country_helper_test.rb b/actionpack/test/template/form_country_helper_test.rb
new file mode 100644
index 0000000000..8862e08222
--- /dev/null
+++ b/actionpack/test/template/form_country_helper_test.rb
@@ -0,0 +1,1549 @@
+require 'abstract_unit'
+
+class FormCountryHelperTest < ActionView::TestCase
+ tests ActionView::Helpers::FormCountryHelper
+
+ silence_warnings do
+ Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin)
+ end
+
+ def test_country_select
+ @post = Post.new
+ @post.origin = "Denmark"
+ expected_select = <<-COUNTRIES
+<select id="post_origin" name="post[origin]"><option value="Afghanistan">Afghanistan</option>
+<option value="Aland Islands">Aland Islands</option>
+<option value="Albania">Albania</option>
+<option value="Algeria">Algeria</option>
+<option value="American Samoa">American Samoa</option>
+<option value="Andorra">Andorra</option>
+<option value="Angola">Angola</option>
+<option value="Anguilla">Anguilla</option>
+<option value="Antarctica">Antarctica</option>
+<option value="Antigua And Barbuda">Antigua And Barbuda</option>
+<option value="Argentina">Argentina</option>
+<option value="Armenia">Armenia</option>
+<option value="Aruba">Aruba</option>
+<option value="Australia">Australia</option>
+<option value="Austria">Austria</option>
+<option value="Azerbaijan">Azerbaijan</option>
+<option value="Bahamas">Bahamas</option>
+<option value="Bahrain">Bahrain</option>
+<option value="Bangladesh">Bangladesh</option>
+<option value="Barbados">Barbados</option>
+<option value="Belarus">Belarus</option>
+<option value="Belgium">Belgium</option>
+<option value="Belize">Belize</option>
+<option value="Benin">Benin</option>
+<option value="Bermuda">Bermuda</option>
+<option value="Bhutan">Bhutan</option>
+<option value="Bolivia">Bolivia</option>
+<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
+<option value="Botswana">Botswana</option>
+<option value="Bouvet Island">Bouvet Island</option>
+<option value="Brazil">Brazil</option>
+<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
+<option value="Brunei Darussalam">Brunei Darussalam</option>
+<option value="Bulgaria">Bulgaria</option>
+<option value="Burkina Faso">Burkina Faso</option>
+<option value="Burundi">Burundi</option>
+<option value="Cambodia">Cambodia</option>
+<option value="Cameroon">Cameroon</option>
+<option value="Canada">Canada</option>
+<option value="Cape Verde">Cape Verde</option>
+<option value="Cayman Islands">Cayman Islands</option>
+<option value="Central African Republic">Central African Republic</option>
+<option value="Chad">Chad</option>
+<option value="Chile">Chile</option>
+<option value="China">China</option>
+<option value="Christmas Island">Christmas Island</option>
+<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
+<option value="Colombia">Colombia</option>
+<option value="Comoros">Comoros</option>
+<option value="Congo">Congo</option>
+<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
+<option value="Cook Islands">Cook Islands</option>
+<option value="Costa Rica">Costa Rica</option>
+<option value="Cote d'Ivoire">Cote d'Ivoire</option>
+<option value="Croatia">Croatia</option>
+<option value="Cuba">Cuba</option>
+<option value="Cyprus">Cyprus</option>
+<option value="Czech Republic">Czech Republic</option>
+<option selected="selected" value="Denmark">Denmark</option>
+<option value="Djibouti">Djibouti</option>
+<option value="Dominica">Dominica</option>
+<option value="Dominican Republic">Dominican Republic</option>
+<option value="Ecuador">Ecuador</option>
+<option value="Egypt">Egypt</option>
+<option value="El Salvador">El Salvador</option>
+<option value="Equatorial Guinea">Equatorial Guinea</option>
+<option value="Eritrea">Eritrea</option>
+<option value="Estonia">Estonia</option>
+<option value="Ethiopia">Ethiopia</option>
+<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
+<option value="Faroe Islands">Faroe Islands</option>
+<option value="Fiji">Fiji</option>
+<option value="Finland">Finland</option>
+<option value="France">France</option>
+<option value="French Guiana">French Guiana</option>
+<option value="French Polynesia">French Polynesia</option>
+<option value="French Southern Territories">French Southern Territories</option>
+<option value="Gabon">Gabon</option>
+<option value="Gambia">Gambia</option>
+<option value="Georgia">Georgia</option>
+<option value="Germany">Germany</option>
+<option value="Ghana">Ghana</option>
+<option value="Gibraltar">Gibraltar</option>
+<option value="Greece">Greece</option>
+<option value="Greenland">Greenland</option>
+<option value="Grenada">Grenada</option>
+<option value="Guadeloupe">Guadeloupe</option>
+<option value="Guam">Guam</option>
+<option value="Guatemala">Guatemala</option>
+<option value="Guernsey">Guernsey</option>
+<option value="Guinea">Guinea</option>
+<option value="Guinea-Bissau">Guinea-Bissau</option>
+<option value="Guyana">Guyana</option>
+<option value="Haiti">Haiti</option>
+<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
+<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
+<option value="Honduras">Honduras</option>
+<option value="Hong Kong">Hong Kong</option>
+<option value="Hungary">Hungary</option>
+<option value="Iceland">Iceland</option>
+<option value="India">India</option>
+<option value="Indonesia">Indonesia</option>
+<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
+<option value="Iraq">Iraq</option>
+<option value="Ireland">Ireland</option>
+<option value="Isle of Man">Isle of Man</option>
+<option value="Israel">Israel</option>
+<option value="Italy">Italy</option>
+<option value="Jamaica">Jamaica</option>
+<option value="Japan">Japan</option>
+<option value="Jersey">Jersey</option>
+<option value="Jordan">Jordan</option>
+<option value="Kazakhstan">Kazakhstan</option>
+<option value="Kenya">Kenya</option>
+<option value="Kiribati">Kiribati</option>
+<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
+<option value="Korea, Republic of">Korea, Republic of</option>
+<option value="Kuwait">Kuwait</option>
+<option value="Kyrgyzstan">Kyrgyzstan</option>
+<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
+<option value="Latvia">Latvia</option>
+<option value="Lebanon">Lebanon</option>
+<option value="Lesotho">Lesotho</option>
+<option value="Liberia">Liberia</option>
+<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
+<option value="Liechtenstein">Liechtenstein</option>
+<option value="Lithuania">Lithuania</option>
+<option value="Luxembourg">Luxembourg</option>
+<option value="Macao">Macao</option>
+<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
+<option value="Madagascar">Madagascar</option>
+<option value="Malawi">Malawi</option>
+<option value="Malaysia">Malaysia</option>
+<option value="Maldives">Maldives</option>
+<option value="Mali">Mali</option>
+<option value="Malta">Malta</option>
+<option value="Marshall Islands">Marshall Islands</option>
+<option value="Martinique">Martinique</option>
+<option value="Mauritania">Mauritania</option>
+<option value="Mauritius">Mauritius</option>
+<option value="Mayotte">Mayotte</option>
+<option value="Mexico">Mexico</option>
+<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
+<option value="Moldova, Republic of">Moldova, Republic of</option>
+<option value="Monaco">Monaco</option>
+<option value="Mongolia">Mongolia</option>
+<option value="Montenegro">Montenegro</option>
+<option value="Montserrat">Montserrat</option>
+<option value="Morocco">Morocco</option>
+<option value="Mozambique">Mozambique</option>
+<option value="Myanmar">Myanmar</option>
+<option value="Namibia">Namibia</option>
+<option value="Nauru">Nauru</option>
+<option value="Nepal">Nepal</option>
+<option value="Netherlands">Netherlands</option>
+<option value="Netherlands Antilles">Netherlands Antilles</option>
+<option value="New Caledonia">New Caledonia</option>
+<option value="New Zealand">New Zealand</option>
+<option value="Nicaragua">Nicaragua</option>
+<option value="Niger">Niger</option>
+<option value="Nigeria">Nigeria</option>
+<option value="Niue">Niue</option>
+<option value="Norfolk Island">Norfolk Island</option>
+<option value="Northern Mariana Islands">Northern Mariana Islands</option>
+<option value="Norway">Norway</option>
+<option value="Oman">Oman</option>
+<option value="Pakistan">Pakistan</option>
+<option value="Palau">Palau</option>
+<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
+<option value="Panama">Panama</option>
+<option value="Papua New Guinea">Papua New Guinea</option>
+<option value="Paraguay">Paraguay</option>
+<option value="Peru">Peru</option>
+<option value="Philippines">Philippines</option>
+<option value="Pitcairn">Pitcairn</option>
+<option value="Poland">Poland</option>
+<option value="Portugal">Portugal</option>
+<option value="Puerto Rico">Puerto Rico</option>
+<option value="Qatar">Qatar</option>
+<option value="Reunion">Reunion</option>
+<option value="Romania">Romania</option>
+<option value="Russian Federation">Russian Federation</option>
+<option value="Rwanda">Rwanda</option>
+<option value="Saint Barthelemy">Saint Barthelemy</option>
+<option value="Saint Helena">Saint Helena</option>
+<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
+<option value="Saint Lucia">Saint Lucia</option>
+<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
+<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
+<option value="Samoa">Samoa</option>
+<option value="San Marino">San Marino</option>
+<option value="Sao Tome and Principe">Sao Tome and Principe</option>
+<option value="Saudi Arabia">Saudi Arabia</option>
+<option value="Senegal">Senegal</option>
+<option value="Serbia">Serbia</option>
+<option value="Seychelles">Seychelles</option>
+<option value="Sierra Leone">Sierra Leone</option>
+<option value="Singapore">Singapore</option>
+<option value="Slovakia">Slovakia</option>
+<option value="Slovenia">Slovenia</option>
+<option value="Solomon Islands">Solomon Islands</option>
+<option value="Somalia">Somalia</option>
+<option value="South Africa">South Africa</option>
+<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
+<option value="Spain">Spain</option>
+<option value="Sri Lanka">Sri Lanka</option>
+<option value="Sudan">Sudan</option>
+<option value="Suriname">Suriname</option>
+<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
+<option value="Swaziland">Swaziland</option>
+<option value="Sweden">Sweden</option>
+<option value="Switzerland">Switzerland</option>
+<option value="Syrian Arab Republic">Syrian Arab Republic</option>
+<option value="Taiwan, Province of China">Taiwan, Province of China</option>
+<option value="Tajikistan">Tajikistan</option>
+<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
+<option value="Thailand">Thailand</option>
+<option value="Timor-Leste">Timor-Leste</option>
+<option value="Togo">Togo</option>
+<option value="Tokelau">Tokelau</option>
+<option value="Tonga">Tonga</option>
+<option value="Trinidad and Tobago">Trinidad and Tobago</option>
+<option value="Tunisia">Tunisia</option>
+<option value="Turkey">Turkey</option>
+<option value="Turkmenistan">Turkmenistan</option>
+<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
+<option value="Tuvalu">Tuvalu</option>
+<option value="Uganda">Uganda</option>
+<option value="Ukraine">Ukraine</option>
+<option value="United Arab Emirates">United Arab Emirates</option>
+<option value="United Kingdom">United Kingdom</option>
+<option value="United States">United States</option>
+<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
+<option value="Uruguay">Uruguay</option>
+<option value="Uzbekistan">Uzbekistan</option>
+<option value="Vanuatu">Vanuatu</option>
+<option value="Venezuela">Venezuela</option>
+<option value="Viet Nam">Viet Nam</option>
+<option value="Virgin Islands, British">Virgin Islands, British</option>
+<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
+<option value="Wallis and Futuna">Wallis and Futuna</option>
+<option value="Western Sahara">Western Sahara</option>
+<option value="Yemen">Yemen</option>
+<option value="Zambia">Zambia</option>
+<option value="Zimbabwe">Zimbabwe</option></select>
+COUNTRIES
+ assert_dom_equal(expected_select[0..-2], country_select("post", "origin"))
+ end
+
+ def test_country_select_with_priority_countries
+ @post = Post.new
+ @post.origin = "Denmark"
+ expected_select = <<-COUNTRIES
+<select id="post_origin" name="post[origin]"><option value="New Zealand">New Zealand</option>
+<option value="Nicaragua">Nicaragua</option><option value="" disabled="disabled">-------------</option>
+<option value="Afghanistan">Afghanistan</option>
+<option value="Aland Islands">Aland Islands</option>
+<option value="Albania">Albania</option>
+<option value="Algeria">Algeria</option>
+<option value="American Samoa">American Samoa</option>
+<option value="Andorra">Andorra</option>
+<option value="Angola">Angola</option>
+<option value="Anguilla">Anguilla</option>
+<option value="Antarctica">Antarctica</option>
+<option value="Antigua And Barbuda">Antigua And Barbuda</option>
+<option value="Argentina">Argentina</option>
+<option value="Armenia">Armenia</option>
+<option value="Aruba">Aruba</option>
+<option value="Australia">Australia</option>
+<option value="Austria">Austria</option>
+<option value="Azerbaijan">Azerbaijan</option>
+<option value="Bahamas">Bahamas</option>
+<option value="Bahrain">Bahrain</option>
+<option value="Bangladesh">Bangladesh</option>
+<option value="Barbados">Barbados</option>
+<option value="Belarus">Belarus</option>
+<option value="Belgium">Belgium</option>
+<option value="Belize">Belize</option>
+<option value="Benin">Benin</option>
+<option value="Bermuda">Bermuda</option>
+<option value="Bhutan">Bhutan</option>
+<option value="Bolivia">Bolivia</option>
+<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
+<option value="Botswana">Botswana</option>
+<option value="Bouvet Island">Bouvet Island</option>
+<option value="Brazil">Brazil</option>
+<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
+<option value="Brunei Darussalam">Brunei Darussalam</option>
+<option value="Bulgaria">Bulgaria</option>
+<option value="Burkina Faso">Burkina Faso</option>
+<option value="Burundi">Burundi</option>
+<option value="Cambodia">Cambodia</option>
+<option value="Cameroon">Cameroon</option>
+<option value="Canada">Canada</option>
+<option value="Cape Verde">Cape Verde</option>
+<option value="Cayman Islands">Cayman Islands</option>
+<option value="Central African Republic">Central African Republic</option>
+<option value="Chad">Chad</option>
+<option value="Chile">Chile</option>
+<option value="China">China</option>
+<option value="Christmas Island">Christmas Island</option>
+<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
+<option value="Colombia">Colombia</option>
+<option value="Comoros">Comoros</option>
+<option value="Congo">Congo</option>
+<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
+<option value="Cook Islands">Cook Islands</option>
+<option value="Costa Rica">Costa Rica</option>
+<option value="Cote d'Ivoire">Cote d'Ivoire</option>
+<option value="Croatia">Croatia</option>
+<option value="Cuba">Cuba</option>
+<option value="Cyprus">Cyprus</option>
+<option value="Czech Republic">Czech Republic</option>
+<option selected="selected" value="Denmark">Denmark</option>
+<option value="Djibouti">Djibouti</option>
+<option value="Dominica">Dominica</option>
+<option value="Dominican Republic">Dominican Republic</option>
+<option value="Ecuador">Ecuador</option>
+<option value="Egypt">Egypt</option>
+<option value="El Salvador">El Salvador</option>
+<option value="Equatorial Guinea">Equatorial Guinea</option>
+<option value="Eritrea">Eritrea</option>
+<option value="Estonia">Estonia</option>
+<option value="Ethiopia">Ethiopia</option>
+<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
+<option value="Faroe Islands">Faroe Islands</option>
+<option value="Fiji">Fiji</option>
+<option value="Finland">Finland</option>
+<option value="France">France</option>
+<option value="French Guiana">French Guiana</option>
+<option value="French Polynesia">French Polynesia</option>
+<option value="French Southern Territories">French Southern Territories</option>
+<option value="Gabon">Gabon</option>
+<option value="Gambia">Gambia</option>
+<option value="Georgia">Georgia</option>
+<option value="Germany">Germany</option>
+<option value="Ghana">Ghana</option>
+<option value="Gibraltar">Gibraltar</option>
+<option value="Greece">Greece</option>
+<option value="Greenland">Greenland</option>
+<option value="Grenada">Grenada</option>
+<option value="Guadeloupe">Guadeloupe</option>
+<option value="Guam">Guam</option>
+<option value="Guatemala">Guatemala</option>
+<option value="Guernsey">Guernsey</option>
+<option value="Guinea">Guinea</option>
+<option value="Guinea-Bissau">Guinea-Bissau</option>
+<option value="Guyana">Guyana</option>
+<option value="Haiti">Haiti</option>
+<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
+<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
+<option value="Honduras">Honduras</option>
+<option value="Hong Kong">Hong Kong</option>
+<option value="Hungary">Hungary</option>
+<option value="Iceland">Iceland</option>
+<option value="India">India</option>
+<option value="Indonesia">Indonesia</option>
+<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
+<option value="Iraq">Iraq</option>
+<option value="Ireland">Ireland</option>
+<option value="Isle of Man">Isle of Man</option>
+<option value="Israel">Israel</option>
+<option value="Italy">Italy</option>
+<option value="Jamaica">Jamaica</option>
+<option value="Japan">Japan</option>
+<option value="Jersey">Jersey</option>
+<option value="Jordan">Jordan</option>
+<option value="Kazakhstan">Kazakhstan</option>
+<option value="Kenya">Kenya</option>
+<option value="Kiribati">Kiribati</option>
+<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
+<option value="Korea, Republic of">Korea, Republic of</option>
+<option value="Kuwait">Kuwait</option>
+<option value="Kyrgyzstan">Kyrgyzstan</option>
+<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
+<option value="Latvia">Latvia</option>
+<option value="Lebanon">Lebanon</option>
+<option value="Lesotho">Lesotho</option>
+<option value="Liberia">Liberia</option>
+<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
+<option value="Liechtenstein">Liechtenstein</option>
+<option value="Lithuania">Lithuania</option>
+<option value="Luxembourg">Luxembourg</option>
+<option value="Macao">Macao</option>
+<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
+<option value="Madagascar">Madagascar</option>
+<option value="Malawi">Malawi</option>
+<option value="Malaysia">Malaysia</option>
+<option value="Maldives">Maldives</option>
+<option value="Mali">Mali</option>
+<option value="Malta">Malta</option>
+<option value="Marshall Islands">Marshall Islands</option>
+<option value="Martinique">Martinique</option>
+<option value="Mauritania">Mauritania</option>
+<option value="Mauritius">Mauritius</option>
+<option value="Mayotte">Mayotte</option>
+<option value="Mexico">Mexico</option>
+<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
+<option value="Moldova, Republic of">Moldova, Republic of</option>
+<option value="Monaco">Monaco</option>
+<option value="Mongolia">Mongolia</option>
+<option value="Montenegro">Montenegro</option>
+<option value="Montserrat">Montserrat</option>
+<option value="Morocco">Morocco</option>
+<option value="Mozambique">Mozambique</option>
+<option value="Myanmar">Myanmar</option>
+<option value="Namibia">Namibia</option>
+<option value="Nauru">Nauru</option>
+<option value="Nepal">Nepal</option>
+<option value="Netherlands">Netherlands</option>
+<option value="Netherlands Antilles">Netherlands Antilles</option>
+<option value="New Caledonia">New Caledonia</option>
+<option value="New Zealand">New Zealand</option>
+<option value="Nicaragua">Nicaragua</option>
+<option value="Niger">Niger</option>
+<option value="Nigeria">Nigeria</option>
+<option value="Niue">Niue</option>
+<option value="Norfolk Island">Norfolk Island</option>
+<option value="Northern Mariana Islands">Northern Mariana Islands</option>
+<option value="Norway">Norway</option>
+<option value="Oman">Oman</option>
+<option value="Pakistan">Pakistan</option>
+<option value="Palau">Palau</option>
+<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
+<option value="Panama">Panama</option>
+<option value="Papua New Guinea">Papua New Guinea</option>
+<option value="Paraguay">Paraguay</option>
+<option value="Peru">Peru</option>
+<option value="Philippines">Philippines</option>
+<option value="Pitcairn">Pitcairn</option>
+<option value="Poland">Poland</option>
+<option value="Portugal">Portugal</option>
+<option value="Puerto Rico">Puerto Rico</option>
+<option value="Qatar">Qatar</option>
+<option value="Reunion">Reunion</option>
+<option value="Romania">Romania</option>
+<option value="Russian Federation">Russian Federation</option>
+<option value="Rwanda">Rwanda</option>
+<option value="Saint Barthelemy">Saint Barthelemy</option>
+<option value="Saint Helena">Saint Helena</option>
+<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
+<option value="Saint Lucia">Saint Lucia</option>
+<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
+<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
+<option value="Samoa">Samoa</option>
+<option value="San Marino">San Marino</option>
+<option value="Sao Tome and Principe">Sao Tome and Principe</option>
+<option value="Saudi Arabia">Saudi Arabia</option>
+<option value="Senegal">Senegal</option>
+<option value="Serbia">Serbia</option>
+<option value="Seychelles">Seychelles</option>
+<option value="Sierra Leone">Sierra Leone</option>
+<option value="Singapore">Singapore</option>
+<option value="Slovakia">Slovakia</option>
+<option value="Slovenia">Slovenia</option>
+<option value="Solomon Islands">Solomon Islands</option>
+<option value="Somalia">Somalia</option>
+<option value="South Africa">South Africa</option>
+<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
+<option value="Spain">Spain</option>
+<option value="Sri Lanka">Sri Lanka</option>
+<option value="Sudan">Sudan</option>
+<option value="Suriname">Suriname</option>
+<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
+<option value="Swaziland">Swaziland</option>
+<option value="Sweden">Sweden</option>
+<option value="Switzerland">Switzerland</option>
+<option value="Syrian Arab Republic">Syrian Arab Republic</option>
+<option value="Taiwan, Province of China">Taiwan, Province of China</option>
+<option value="Tajikistan">Tajikistan</option>
+<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
+<option value="Thailand">Thailand</option>
+<option value="Timor-Leste">Timor-Leste</option>
+<option value="Togo">Togo</option>
+<option value="Tokelau">Tokelau</option>
+<option value="Tonga">Tonga</option>
+<option value="Trinidad and Tobago">Trinidad and Tobago</option>
+<option value="Tunisia">Tunisia</option>
+<option value="Turkey">Turkey</option>
+<option value="Turkmenistan">Turkmenistan</option>
+<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
+<option value="Tuvalu">Tuvalu</option>
+<option value="Uganda">Uganda</option>
+<option value="Ukraine">Ukraine</option>
+<option value="United Arab Emirates">United Arab Emirates</option>
+<option value="United Kingdom">United Kingdom</option>
+<option value="United States">United States</option>
+<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
+<option value="Uruguay">Uruguay</option>
+<option value="Uzbekistan">Uzbekistan</option>
+<option value="Vanuatu">Vanuatu</option>
+<option value="Venezuela">Venezuela</option>
+<option value="Viet Nam">Viet Nam</option>
+<option value="Virgin Islands, British">Virgin Islands, British</option>
+<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
+<option value="Wallis and Futuna">Wallis and Futuna</option>
+<option value="Western Sahara">Western Sahara</option>
+<option value="Yemen">Yemen</option>
+<option value="Zambia">Zambia</option>
+<option value="Zimbabwe">Zimbabwe</option></select>
+COUNTRIES
+ assert_dom_equal(expected_select[0..-2], country_select("post", "origin", ["New Zealand", "Nicaragua"]))
+ end
+
+ def test_country_select_with_selected_priority_country
+ @post = Post.new
+ @post.origin = "New Zealand"
+ expected_select = <<-COUNTRIES
+<select id="post_origin" name="post[origin]"><option selected="selected" value="New Zealand">New Zealand</option>
+<option value="Nicaragua">Nicaragua</option><option value="" disabled="disabled">-------------</option>
+<option value="Afghanistan">Afghanistan</option>
+<option value="Aland Islands">Aland Islands</option>
+<option value="Albania">Albania</option>
+<option value="Algeria">Algeria</option>
+<option value="American Samoa">American Samoa</option>
+<option value="Andorra">Andorra</option>
+<option value="Angola">Angola</option>
+<option value="Anguilla">Anguilla</option>
+<option value="Antarctica">Antarctica</option>
+<option value="Antigua And Barbuda">Antigua And Barbuda</option>
+<option value="Argentina">Argentina</option>
+<option value="Armenia">Armenia</option>
+<option value="Aruba">Aruba</option>
+<option value="Australia">Australia</option>
+<option value="Austria">Austria</option>
+<option value="Azerbaijan">Azerbaijan</option>
+<option value="Bahamas">Bahamas</option>
+<option value="Bahrain">Bahrain</option>
+<option value="Bangladesh">Bangladesh</option>
+<option value="Barbados">Barbados</option>
+<option value="Belarus">Belarus</option>
+<option value="Belgium">Belgium</option>
+<option value="Belize">Belize</option>
+<option value="Benin">Benin</option>
+<option value="Bermuda">Bermuda</option>
+<option value="Bhutan">Bhutan</option>
+<option value="Bolivia">Bolivia</option>
+<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
+<option value="Botswana">Botswana</option>
+<option value="Bouvet Island">Bouvet Island</option>
+<option value="Brazil">Brazil</option>
+<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
+<option value="Brunei Darussalam">Brunei Darussalam</option>
+<option value="Bulgaria">Bulgaria</option>
+<option value="Burkina Faso">Burkina Faso</option>
+<option value="Burundi">Burundi</option>
+<option value="Cambodia">Cambodia</option>
+<option value="Cameroon">Cameroon</option>
+<option value="Canada">Canada</option>
+<option value="Cape Verde">Cape Verde</option>
+<option value="Cayman Islands">Cayman Islands</option>
+<option value="Central African Republic">Central African Republic</option>
+<option value="Chad">Chad</option>
+<option value="Chile">Chile</option>
+<option value="China">China</option>
+<option value="Christmas Island">Christmas Island</option>
+<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
+<option value="Colombia">Colombia</option>
+<option value="Comoros">Comoros</option>
+<option value="Congo">Congo</option>
+<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
+<option value="Cook Islands">Cook Islands</option>
+<option value="Costa Rica">Costa Rica</option>
+<option value="Cote d'Ivoire">Cote d'Ivoire</option>
+<option value="Croatia">Croatia</option>
+<option value="Cuba">Cuba</option>
+<option value="Cyprus">Cyprus</option>
+<option value="Czech Republic">Czech Republic</option>
+<option value="Denmark">Denmark</option>
+<option value="Djibouti">Djibouti</option>
+<option value="Dominica">Dominica</option>
+<option value="Dominican Republic">Dominican Republic</option>
+<option value="Ecuador">Ecuador</option>
+<option value="Egypt">Egypt</option>
+<option value="El Salvador">El Salvador</option>
+<option value="Equatorial Guinea">Equatorial Guinea</option>
+<option value="Eritrea">Eritrea</option>
+<option value="Estonia">Estonia</option>
+<option value="Ethiopia">Ethiopia</option>
+<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
+<option value="Faroe Islands">Faroe Islands</option>
+<option value="Fiji">Fiji</option>
+<option value="Finland">Finland</option>
+<option value="France">France</option>
+<option value="French Guiana">French Guiana</option>
+<option value="French Polynesia">French Polynesia</option>
+<option value="French Southern Territories">French Southern Territories</option>
+<option value="Gabon">Gabon</option>
+<option value="Gambia">Gambia</option>
+<option value="Georgia">Georgia</option>
+<option value="Germany">Germany</option>
+<option value="Ghana">Ghana</option>
+<option value="Gibraltar">Gibraltar</option>
+<option value="Greece">Greece</option>
+<option value="Greenland">Greenland</option>
+<option value="Grenada">Grenada</option>
+<option value="Guadeloupe">Guadeloupe</option>
+<option value="Guam">Guam</option>
+<option value="Guatemala">Guatemala</option>
+<option value="Guernsey">Guernsey</option>
+<option value="Guinea">Guinea</option>
+<option value="Guinea-Bissau">Guinea-Bissau</option>
+<option value="Guyana">Guyana</option>
+<option value="Haiti">Haiti</option>
+<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
+<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
+<option value="Honduras">Honduras</option>
+<option value="Hong Kong">Hong Kong</option>
+<option value="Hungary">Hungary</option>
+<option value="Iceland">Iceland</option>
+<option value="India">India</option>
+<option value="Indonesia">Indonesia</option>
+<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
+<option value="Iraq">Iraq</option>
+<option value="Ireland">Ireland</option>
+<option value="Isle of Man">Isle of Man</option>
+<option value="Israel">Israel</option>
+<option value="Italy">Italy</option>
+<option value="Jamaica">Jamaica</option>
+<option value="Japan">Japan</option>
+<option value="Jersey">Jersey</option>
+<option value="Jordan">Jordan</option>
+<option value="Kazakhstan">Kazakhstan</option>
+<option value="Kenya">Kenya</option>
+<option value="Kiribati">Kiribati</option>
+<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
+<option value="Korea, Republic of">Korea, Republic of</option>
+<option value="Kuwait">Kuwait</option>
+<option value="Kyrgyzstan">Kyrgyzstan</option>
+<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
+<option value="Latvia">Latvia</option>
+<option value="Lebanon">Lebanon</option>
+<option value="Lesotho">Lesotho</option>
+<option value="Liberia">Liberia</option>
+<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
+<option value="Liechtenstein">Liechtenstein</option>
+<option value="Lithuania">Lithuania</option>
+<option value="Luxembourg">Luxembourg</option>
+<option value="Macao">Macao</option>
+<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
+<option value="Madagascar">Madagascar</option>
+<option value="Malawi">Malawi</option>
+<option value="Malaysia">Malaysia</option>
+<option value="Maldives">Maldives</option>
+<option value="Mali">Mali</option>
+<option value="Malta">Malta</option>
+<option value="Marshall Islands">Marshall Islands</option>
+<option value="Martinique">Martinique</option>
+<option value="Mauritania">Mauritania</option>
+<option value="Mauritius">Mauritius</option>
+<option value="Mayotte">Mayotte</option>
+<option value="Mexico">Mexico</option>
+<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
+<option value="Moldova, Republic of">Moldova, Republic of</option>
+<option value="Monaco">Monaco</option>
+<option value="Mongolia">Mongolia</option>
+<option value="Montenegro">Montenegro</option>
+<option value="Montserrat">Montserrat</option>
+<option value="Morocco">Morocco</option>
+<option value="Mozambique">Mozambique</option>
+<option value="Myanmar">Myanmar</option>
+<option value="Namibia">Namibia</option>
+<option value="Nauru">Nauru</option>
+<option value="Nepal">Nepal</option>
+<option value="Netherlands">Netherlands</option>
+<option value="Netherlands Antilles">Netherlands Antilles</option>
+<option value="New Caledonia">New Caledonia</option>
+<option selected="selected" value="New Zealand">New Zealand</option>
+<option value="Nicaragua">Nicaragua</option>
+<option value="Niger">Niger</option>
+<option value="Nigeria">Nigeria</option>
+<option value="Niue">Niue</option>
+<option value="Norfolk Island">Norfolk Island</option>
+<option value="Northern Mariana Islands">Northern Mariana Islands</option>
+<option value="Norway">Norway</option>
+<option value="Oman">Oman</option>
+<option value="Pakistan">Pakistan</option>
+<option value="Palau">Palau</option>
+<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
+<option value="Panama">Panama</option>
+<option value="Papua New Guinea">Papua New Guinea</option>
+<option value="Paraguay">Paraguay</option>
+<option value="Peru">Peru</option>
+<option value="Philippines">Philippines</option>
+<option value="Pitcairn">Pitcairn</option>
+<option value="Poland">Poland</option>
+<option value="Portugal">Portugal</option>
+<option value="Puerto Rico">Puerto Rico</option>
+<option value="Qatar">Qatar</option>
+<option value="Reunion">Reunion</option>
+<option value="Romania">Romania</option>
+<option value="Russian Federation">Russian Federation</option>
+<option value="Rwanda">Rwanda</option>
+<option value="Saint Barthelemy">Saint Barthelemy</option>
+<option value="Saint Helena">Saint Helena</option>
+<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
+<option value="Saint Lucia">Saint Lucia</option>
+<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
+<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
+<option value="Samoa">Samoa</option>
+<option value="San Marino">San Marino</option>
+<option value="Sao Tome and Principe">Sao Tome and Principe</option>
+<option value="Saudi Arabia">Saudi Arabia</option>
+<option value="Senegal">Senegal</option>
+<option value="Serbia">Serbia</option>
+<option value="Seychelles">Seychelles</option>
+<option value="Sierra Leone">Sierra Leone</option>
+<option value="Singapore">Singapore</option>
+<option value="Slovakia">Slovakia</option>
+<option value="Slovenia">Slovenia</option>
+<option value="Solomon Islands">Solomon Islands</option>
+<option value="Somalia">Somalia</option>
+<option value="South Africa">South Africa</option>
+<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
+<option value="Spain">Spain</option>
+<option value="Sri Lanka">Sri Lanka</option>
+<option value="Sudan">Sudan</option>
+<option value="Suriname">Suriname</option>
+<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
+<option value="Swaziland">Swaziland</option>
+<option value="Sweden">Sweden</option>
+<option value="Switzerland">Switzerland</option>
+<option value="Syrian Arab Republic">Syrian Arab Republic</option>
+<option value="Taiwan, Province of China">Taiwan, Province of China</option>
+<option value="Tajikistan">Tajikistan</option>
+<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
+<option value="Thailand">Thailand</option>
+<option value="Timor-Leste">Timor-Leste</option>
+<option value="Togo">Togo</option>
+<option value="Tokelau">Tokelau</option>
+<option value="Tonga">Tonga</option>
+<option value="Trinidad and Tobago">Trinidad and Tobago</option>
+<option value="Tunisia">Tunisia</option>
+<option value="Turkey">Turkey</option>
+<option value="Turkmenistan">Turkmenistan</option>
+<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
+<option value="Tuvalu">Tuvalu</option>
+<option value="Uganda">Uganda</option>
+<option value="Ukraine">Ukraine</option>
+<option value="United Arab Emirates">United Arab Emirates</option>
+<option value="United Kingdom">United Kingdom</option>
+<option value="United States">United States</option>
+<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
+<option value="Uruguay">Uruguay</option>
+<option value="Uzbekistan">Uzbekistan</option>
+<option value="Vanuatu">Vanuatu</option>
+<option value="Venezuela">Venezuela</option>
+<option value="Viet Nam">Viet Nam</option>
+<option value="Virgin Islands, British">Virgin Islands, British</option>
+<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
+<option value="Wallis and Futuna">Wallis and Futuna</option>
+<option value="Western Sahara">Western Sahara</option>
+<option value="Yemen">Yemen</option>
+<option value="Zambia">Zambia</option>
+<option value="Zimbabwe">Zimbabwe</option></select>
+COUNTRIES
+ assert_dom_equal(expected_select[0..-2], country_select("post", "origin", ["New Zealand", "Nicaragua"]))
+ end
+
+ def test_country_select_under_fields_for
+ @post = Post.new
+ @post.origin = "Australia"
+ expected_select = <<-COUNTRIES
+<select id="post_origin" name="post[origin]"><option value="Afghanistan">Afghanistan</option>
+<option value="Aland Islands">Aland Islands</option>
+<option value="Albania">Albania</option>
+<option value="Algeria">Algeria</option>
+<option value="American Samoa">American Samoa</option>
+<option value="Andorra">Andorra</option>
+<option value="Angola">Angola</option>
+<option value="Anguilla">Anguilla</option>
+<option value="Antarctica">Antarctica</option>
+<option value="Antigua And Barbuda">Antigua And Barbuda</option>
+<option value="Argentina">Argentina</option>
+<option value="Armenia">Armenia</option>
+<option value="Aruba">Aruba</option>
+<option selected="selected" value="Australia">Australia</option>
+<option value="Austria">Austria</option>
+<option value="Azerbaijan">Azerbaijan</option>
+<option value="Bahamas">Bahamas</option>
+<option value="Bahrain">Bahrain</option>
+<option value="Bangladesh">Bangladesh</option>
+<option value="Barbados">Barbados</option>
+<option value="Belarus">Belarus</option>
+<option value="Belgium">Belgium</option>
+<option value="Belize">Belize</option>
+<option value="Benin">Benin</option>
+<option value="Bermuda">Bermuda</option>
+<option value="Bhutan">Bhutan</option>
+<option value="Bolivia">Bolivia</option>
+<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
+<option value="Botswana">Botswana</option>
+<option value="Bouvet Island">Bouvet Island</option>
+<option value="Brazil">Brazil</option>
+<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
+<option value="Brunei Darussalam">Brunei Darussalam</option>
+<option value="Bulgaria">Bulgaria</option>
+<option value="Burkina Faso">Burkina Faso</option>
+<option value="Burundi">Burundi</option>
+<option value="Cambodia">Cambodia</option>
+<option value="Cameroon">Cameroon</option>
+<option value="Canada">Canada</option>
+<option value="Cape Verde">Cape Verde</option>
+<option value="Cayman Islands">Cayman Islands</option>
+<option value="Central African Republic">Central African Republic</option>
+<option value="Chad">Chad</option>
+<option value="Chile">Chile</option>
+<option value="China">China</option>
+<option value="Christmas Island">Christmas Island</option>
+<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
+<option value="Colombia">Colombia</option>
+<option value="Comoros">Comoros</option>
+<option value="Congo">Congo</option>
+<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
+<option value="Cook Islands">Cook Islands</option>
+<option value="Costa Rica">Costa Rica</option>
+<option value="Cote d'Ivoire">Cote d'Ivoire</option>
+<option value="Croatia">Croatia</option>
+<option value="Cuba">Cuba</option>
+<option value="Cyprus">Cyprus</option>
+<option value="Czech Republic">Czech Republic</option>
+<option value="Denmark">Denmark</option>
+<option value="Djibouti">Djibouti</option>
+<option value="Dominica">Dominica</option>
+<option value="Dominican Republic">Dominican Republic</option>
+<option value="Ecuador">Ecuador</option>
+<option value="Egypt">Egypt</option>
+<option value="El Salvador">El Salvador</option>
+<option value="Equatorial Guinea">Equatorial Guinea</option>
+<option value="Eritrea">Eritrea</option>
+<option value="Estonia">Estonia</option>
+<option value="Ethiopia">Ethiopia</option>
+<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
+<option value="Faroe Islands">Faroe Islands</option>
+<option value="Fiji">Fiji</option>
+<option value="Finland">Finland</option>
+<option value="France">France</option>
+<option value="French Guiana">French Guiana</option>
+<option value="French Polynesia">French Polynesia</option>
+<option value="French Southern Territories">French Southern Territories</option>
+<option value="Gabon">Gabon</option>
+<option value="Gambia">Gambia</option>
+<option value="Georgia">Georgia</option>
+<option value="Germany">Germany</option>
+<option value="Ghana">Ghana</option>
+<option value="Gibraltar">Gibraltar</option>
+<option value="Greece">Greece</option>
+<option value="Greenland">Greenland</option>
+<option value="Grenada">Grenada</option>
+<option value="Guadeloupe">Guadeloupe</option>
+<option value="Guam">Guam</option>
+<option value="Guatemala">Guatemala</option>
+<option value="Guernsey">Guernsey</option>
+<option value="Guinea">Guinea</option>
+<option value="Guinea-Bissau">Guinea-Bissau</option>
+<option value="Guyana">Guyana</option>
+<option value="Haiti">Haiti</option>
+<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
+<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
+<option value="Honduras">Honduras</option>
+<option value="Hong Kong">Hong Kong</option>
+<option value="Hungary">Hungary</option>
+<option value="Iceland">Iceland</option>
+<option value="India">India</option>
+<option value="Indonesia">Indonesia</option>
+<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
+<option value="Iraq">Iraq</option>
+<option value="Ireland">Ireland</option>
+<option value="Isle of Man">Isle of Man</option>
+<option value="Israel">Israel</option>
+<option value="Italy">Italy</option>
+<option value="Jamaica">Jamaica</option>
+<option value="Japan">Japan</option>
+<option value="Jersey">Jersey</option>
+<option value="Jordan">Jordan</option>
+<option value="Kazakhstan">Kazakhstan</option>
+<option value="Kenya">Kenya</option>
+<option value="Kiribati">Kiribati</option>
+<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
+<option value="Korea, Republic of">Korea, Republic of</option>
+<option value="Kuwait">Kuwait</option>
+<option value="Kyrgyzstan">Kyrgyzstan</option>
+<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
+<option value="Latvia">Latvia</option>
+<option value="Lebanon">Lebanon</option>
+<option value="Lesotho">Lesotho</option>
+<option value="Liberia">Liberia</option>
+<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
+<option value="Liechtenstein">Liechtenstein</option>
+<option value="Lithuania">Lithuania</option>
+<option value="Luxembourg">Luxembourg</option>
+<option value="Macao">Macao</option>
+<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
+<option value="Madagascar">Madagascar</option>
+<option value="Malawi">Malawi</option>
+<option value="Malaysia">Malaysia</option>
+<option value="Maldives">Maldives</option>
+<option value="Mali">Mali</option>
+<option value="Malta">Malta</option>
+<option value="Marshall Islands">Marshall Islands</option>
+<option value="Martinique">Martinique</option>
+<option value="Mauritania">Mauritania</option>
+<option value="Mauritius">Mauritius</option>
+<option value="Mayotte">Mayotte</option>
+<option value="Mexico">Mexico</option>
+<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
+<option value="Moldova, Republic of">Moldova, Republic of</option>
+<option value="Monaco">Monaco</option>
+<option value="Mongolia">Mongolia</option>
+<option value="Montenegro">Montenegro</option>
+<option value="Montserrat">Montserrat</option>
+<option value="Morocco">Morocco</option>
+<option value="Mozambique">Mozambique</option>
+<option value="Myanmar">Myanmar</option>
+<option value="Namibia">Namibia</option>
+<option value="Nauru">Nauru</option>
+<option value="Nepal">Nepal</option>
+<option value="Netherlands">Netherlands</option>
+<option value="Netherlands Antilles">Netherlands Antilles</option>
+<option value="New Caledonia">New Caledonia</option>
+<option value="New Zealand">New Zealand</option>
+<option value="Nicaragua">Nicaragua</option>
+<option value="Niger">Niger</option>
+<option value="Nigeria">Nigeria</option>
+<option value="Niue">Niue</option>
+<option value="Norfolk Island">Norfolk Island</option>
+<option value="Northern Mariana Islands">Northern Mariana Islands</option>
+<option value="Norway">Norway</option>
+<option value="Oman">Oman</option>
+<option value="Pakistan">Pakistan</option>
+<option value="Palau">Palau</option>
+<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
+<option value="Panama">Panama</option>
+<option value="Papua New Guinea">Papua New Guinea</option>
+<option value="Paraguay">Paraguay</option>
+<option value="Peru">Peru</option>
+<option value="Philippines">Philippines</option>
+<option value="Pitcairn">Pitcairn</option>
+<option value="Poland">Poland</option>
+<option value="Portugal">Portugal</option>
+<option value="Puerto Rico">Puerto Rico</option>
+<option value="Qatar">Qatar</option>
+<option value="Reunion">Reunion</option>
+<option value="Romania">Romania</option>
+<option value="Russian Federation">Russian Federation</option>
+<option value="Rwanda">Rwanda</option>
+<option value="Saint Barthelemy">Saint Barthelemy</option>
+<option value="Saint Helena">Saint Helena</option>
+<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
+<option value="Saint Lucia">Saint Lucia</option>
+<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
+<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
+<option value="Samoa">Samoa</option>
+<option value="San Marino">San Marino</option>
+<option value="Sao Tome and Principe">Sao Tome and Principe</option>
+<option value="Saudi Arabia">Saudi Arabia</option>
+<option value="Senegal">Senegal</option>
+<option value="Serbia">Serbia</option>
+<option value="Seychelles">Seychelles</option>
+<option value="Sierra Leone">Sierra Leone</option>
+<option value="Singapore">Singapore</option>
+<option value="Slovakia">Slovakia</option>
+<option value="Slovenia">Slovenia</option>
+<option value="Solomon Islands">Solomon Islands</option>
+<option value="Somalia">Somalia</option>
+<option value="South Africa">South Africa</option>
+<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
+<option value="Spain">Spain</option>
+<option value="Sri Lanka">Sri Lanka</option>
+<option value="Sudan">Sudan</option>
+<option value="Suriname">Suriname</option>
+<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
+<option value="Swaziland">Swaziland</option>
+<option value="Sweden">Sweden</option>
+<option value="Switzerland">Switzerland</option>
+<option value="Syrian Arab Republic">Syrian Arab Republic</option>
+<option value="Taiwan, Province of China">Taiwan, Province of China</option>
+<option value="Tajikistan">Tajikistan</option>
+<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
+<option value="Thailand">Thailand</option>
+<option value="Timor-Leste">Timor-Leste</option>
+<option value="Togo">Togo</option>
+<option value="Tokelau">Tokelau</option>
+<option value="Tonga">Tonga</option>
+<option value="Trinidad and Tobago">Trinidad and Tobago</option>
+<option value="Tunisia">Tunisia</option>
+<option value="Turkey">Turkey</option>
+<option value="Turkmenistan">Turkmenistan</option>
+<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
+<option value="Tuvalu">Tuvalu</option>
+<option value="Uganda">Uganda</option>
+<option value="Ukraine">Ukraine</option>
+<option value="United Arab Emirates">United Arab Emirates</option>
+<option value="United Kingdom">United Kingdom</option>
+<option value="United States">United States</option>
+<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
+<option value="Uruguay">Uruguay</option>
+<option value="Uzbekistan">Uzbekistan</option>
+<option value="Vanuatu">Vanuatu</option>
+<option value="Venezuela">Venezuela</option>
+<option value="Viet Nam">Viet Nam</option>
+<option value="Virgin Islands, British">Virgin Islands, British</option>
+<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
+<option value="Wallis and Futuna">Wallis and Futuna</option>
+<option value="Western Sahara">Western Sahara</option>
+<option value="Yemen">Yemen</option>
+<option value="Zambia">Zambia</option>
+<option value="Zimbabwe">Zimbabwe</option></select>
+COUNTRIES
+
+ fields_for :post, @post do |f|
+ concat f.country_select("origin")
+ end
+
+ assert_dom_equal(expected_select[0..-2], output_buffer)
+ end
+
+ def test_country_select_under_fields_for_with_index
+ @post = Post.new
+ @post.origin = "United States"
+ expected_select = <<-COUNTRIES
+<select id="post_325_origin" name="post[325][origin]"><option value="Afghanistan">Afghanistan</option>
+<option value="Aland Islands">Aland Islands</option>
+<option value="Albania">Albania</option>
+<option value="Algeria">Algeria</option>
+<option value="American Samoa">American Samoa</option>
+<option value="Andorra">Andorra</option>
+<option value="Angola">Angola</option>
+<option value="Anguilla">Anguilla</option>
+<option value="Antarctica">Antarctica</option>
+<option value="Antigua And Barbuda">Antigua And Barbuda</option>
+<option value="Argentina">Argentina</option>
+<option value="Armenia">Armenia</option>
+<option value="Aruba">Aruba</option>
+<option value="Australia">Australia</option>
+<option value="Austria">Austria</option>
+<option value="Azerbaijan">Azerbaijan</option>
+<option value="Bahamas">Bahamas</option>
+<option value="Bahrain">Bahrain</option>
+<option value="Bangladesh">Bangladesh</option>
+<option value="Barbados">Barbados</option>
+<option value="Belarus">Belarus</option>
+<option value="Belgium">Belgium</option>
+<option value="Belize">Belize</option>
+<option value="Benin">Benin</option>
+<option value="Bermuda">Bermuda</option>
+<option value="Bhutan">Bhutan</option>
+<option value="Bolivia">Bolivia</option>
+<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
+<option value="Botswana">Botswana</option>
+<option value="Bouvet Island">Bouvet Island</option>
+<option value="Brazil">Brazil</option>
+<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
+<option value="Brunei Darussalam">Brunei Darussalam</option>
+<option value="Bulgaria">Bulgaria</option>
+<option value="Burkina Faso">Burkina Faso</option>
+<option value="Burundi">Burundi</option>
+<option value="Cambodia">Cambodia</option>
+<option value="Cameroon">Cameroon</option>
+<option value="Canada">Canada</option>
+<option value="Cape Verde">Cape Verde</option>
+<option value="Cayman Islands">Cayman Islands</option>
+<option value="Central African Republic">Central African Republic</option>
+<option value="Chad">Chad</option>
+<option value="Chile">Chile</option>
+<option value="China">China</option>
+<option value="Christmas Island">Christmas Island</option>
+<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
+<option value="Colombia">Colombia</option>
+<option value="Comoros">Comoros</option>
+<option value="Congo">Congo</option>
+<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
+<option value="Cook Islands">Cook Islands</option>
+<option value="Costa Rica">Costa Rica</option>
+<option value="Cote d'Ivoire">Cote d'Ivoire</option>
+<option value="Croatia">Croatia</option>
+<option value="Cuba">Cuba</option>
+<option value="Cyprus">Cyprus</option>
+<option value="Czech Republic">Czech Republic</option>
+<option value="Denmark">Denmark</option>
+<option value="Djibouti">Djibouti</option>
+<option value="Dominica">Dominica</option>
+<option value="Dominican Republic">Dominican Republic</option>
+<option value="Ecuador">Ecuador</option>
+<option value="Egypt">Egypt</option>
+<option value="El Salvador">El Salvador</option>
+<option value="Equatorial Guinea">Equatorial Guinea</option>
+<option value="Eritrea">Eritrea</option>
+<option value="Estonia">Estonia</option>
+<option value="Ethiopia">Ethiopia</option>
+<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
+<option value="Faroe Islands">Faroe Islands</option>
+<option value="Fiji">Fiji</option>
+<option value="Finland">Finland</option>
+<option value="France">France</option>
+<option value="French Guiana">French Guiana</option>
+<option value="French Polynesia">French Polynesia</option>
+<option value="French Southern Territories">French Southern Territories</option>
+<option value="Gabon">Gabon</option>
+<option value="Gambia">Gambia</option>
+<option value="Georgia">Georgia</option>
+<option value="Germany">Germany</option>
+<option value="Ghana">Ghana</option>
+<option value="Gibraltar">Gibraltar</option>
+<option value="Greece">Greece</option>
+<option value="Greenland">Greenland</option>
+<option value="Grenada">Grenada</option>
+<option value="Guadeloupe">Guadeloupe</option>
+<option value="Guam">Guam</option>
+<option value="Guatemala">Guatemala</option>
+<option value="Guernsey">Guernsey</option>
+<option value="Guinea">Guinea</option>
+<option value="Guinea-Bissau">Guinea-Bissau</option>
+<option value="Guyana">Guyana</option>
+<option value="Haiti">Haiti</option>
+<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
+<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
+<option value="Honduras">Honduras</option>
+<option value="Hong Kong">Hong Kong</option>
+<option value="Hungary">Hungary</option>
+<option value="Iceland">Iceland</option>
+<option value="India">India</option>
+<option value="Indonesia">Indonesia</option>
+<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
+<option value="Iraq">Iraq</option>
+<option value="Ireland">Ireland</option>
+<option value="Isle of Man">Isle of Man</option>
+<option value="Israel">Israel</option>
+<option value="Italy">Italy</option>
+<option value="Jamaica">Jamaica</option>
+<option value="Japan">Japan</option>
+<option value="Jersey">Jersey</option>
+<option value="Jordan">Jordan</option>
+<option value="Kazakhstan">Kazakhstan</option>
+<option value="Kenya">Kenya</option>
+<option value="Kiribati">Kiribati</option>
+<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
+<option value="Korea, Republic of">Korea, Republic of</option>
+<option value="Kuwait">Kuwait</option>
+<option value="Kyrgyzstan">Kyrgyzstan</option>
+<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
+<option value="Latvia">Latvia</option>
+<option value="Lebanon">Lebanon</option>
+<option value="Lesotho">Lesotho</option>
+<option value="Liberia">Liberia</option>
+<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
+<option value="Liechtenstein">Liechtenstein</option>
+<option value="Lithuania">Lithuania</option>
+<option value="Luxembourg">Luxembourg</option>
+<option value="Macao">Macao</option>
+<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
+<option value="Madagascar">Madagascar</option>
+<option value="Malawi">Malawi</option>
+<option value="Malaysia">Malaysia</option>
+<option value="Maldives">Maldives</option>
+<option value="Mali">Mali</option>
+<option value="Malta">Malta</option>
+<option value="Marshall Islands">Marshall Islands</option>
+<option value="Martinique">Martinique</option>
+<option value="Mauritania">Mauritania</option>
+<option value="Mauritius">Mauritius</option>
+<option value="Mayotte">Mayotte</option>
+<option value="Mexico">Mexico</option>
+<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
+<option value="Moldova, Republic of">Moldova, Republic of</option>
+<option value="Monaco">Monaco</option>
+<option value="Mongolia">Mongolia</option>
+<option value="Montenegro">Montenegro</option>
+<option value="Montserrat">Montserrat</option>
+<option value="Morocco">Morocco</option>
+<option value="Mozambique">Mozambique</option>
+<option value="Myanmar">Myanmar</option>
+<option value="Namibia">Namibia</option>
+<option value="Nauru">Nauru</option>
+<option value="Nepal">Nepal</option>
+<option value="Netherlands">Netherlands</option>
+<option value="Netherlands Antilles">Netherlands Antilles</option>
+<option value="New Caledonia">New Caledonia</option>
+<option value="New Zealand">New Zealand</option>
+<option value="Nicaragua">Nicaragua</option>
+<option value="Niger">Niger</option>
+<option value="Nigeria">Nigeria</option>
+<option value="Niue">Niue</option>
+<option value="Norfolk Island">Norfolk Island</option>
+<option value="Northern Mariana Islands">Northern Mariana Islands</option>
+<option value="Norway">Norway</option>
+<option value="Oman">Oman</option>
+<option value="Pakistan">Pakistan</option>
+<option value="Palau">Palau</option>
+<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
+<option value="Panama">Panama</option>
+<option value="Papua New Guinea">Papua New Guinea</option>
+<option value="Paraguay">Paraguay</option>
+<option value="Peru">Peru</option>
+<option value="Philippines">Philippines</option>
+<option value="Pitcairn">Pitcairn</option>
+<option value="Poland">Poland</option>
+<option value="Portugal">Portugal</option>
+<option value="Puerto Rico">Puerto Rico</option>
+<option value="Qatar">Qatar</option>
+<option value="Reunion">Reunion</option>
+<option value="Romania">Romania</option>
+<option value="Russian Federation">Russian Federation</option>
+<option value="Rwanda">Rwanda</option>
+<option value="Saint Barthelemy">Saint Barthelemy</option>
+<option value="Saint Helena">Saint Helena</option>
+<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
+<option value="Saint Lucia">Saint Lucia</option>
+<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
+<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
+<option value="Samoa">Samoa</option>
+<option value="San Marino">San Marino</option>
+<option value="Sao Tome and Principe">Sao Tome and Principe</option>
+<option value="Saudi Arabia">Saudi Arabia</option>
+<option value="Senegal">Senegal</option>
+<option value="Serbia">Serbia</option>
+<option value="Seychelles">Seychelles</option>
+<option value="Sierra Leone">Sierra Leone</option>
+<option value="Singapore">Singapore</option>
+<option value="Slovakia">Slovakia</option>
+<option value="Slovenia">Slovenia</option>
+<option value="Solomon Islands">Solomon Islands</option>
+<option value="Somalia">Somalia</option>
+<option value="South Africa">South Africa</option>
+<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
+<option value="Spain">Spain</option>
+<option value="Sri Lanka">Sri Lanka</option>
+<option value="Sudan">Sudan</option>
+<option value="Suriname">Suriname</option>
+<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
+<option value="Swaziland">Swaziland</option>
+<option value="Sweden">Sweden</option>
+<option value="Switzerland">Switzerland</option>
+<option value="Syrian Arab Republic">Syrian Arab Republic</option>
+<option value="Taiwan, Province of China">Taiwan, Province of China</option>
+<option value="Tajikistan">Tajikistan</option>
+<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
+<option value="Thailand">Thailand</option>
+<option value="Timor-Leste">Timor-Leste</option>
+<option value="Togo">Togo</option>
+<option value="Tokelau">Tokelau</option>
+<option value="Tonga">Tonga</option>
+<option value="Trinidad and Tobago">Trinidad and Tobago</option>
+<option value="Tunisia">Tunisia</option>
+<option value="Turkey">Turkey</option>
+<option value="Turkmenistan">Turkmenistan</option>
+<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
+<option value="Tuvalu">Tuvalu</option>
+<option value="Uganda">Uganda</option>
+<option value="Ukraine">Ukraine</option>
+<option value="United Arab Emirates">United Arab Emirates</option>
+<option value="United Kingdom">United Kingdom</option>
+<option selected="selected" value="United States">United States</option>
+<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
+<option value="Uruguay">Uruguay</option>
+<option value="Uzbekistan">Uzbekistan</option>
+<option value="Vanuatu">Vanuatu</option>
+<option value="Venezuela">Venezuela</option>
+<option value="Viet Nam">Viet Nam</option>
+<option value="Virgin Islands, British">Virgin Islands, British</option>
+<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
+<option value="Wallis and Futuna">Wallis and Futuna</option>
+<option value="Western Sahara">Western Sahara</option>
+<option value="Yemen">Yemen</option>
+<option value="Zambia">Zambia</option>
+<option value="Zimbabwe">Zimbabwe</option></select>
+COUNTRIES
+
+ fields_for :post, @post, :index => 325 do |f|
+ concat f.country_select("origin")
+ end
+
+ assert_dom_equal(expected_select[0..-2], output_buffer)
+ end
+
+ def test_country_select_under_fields_for_with_auto_index
+ @post = Post.new
+ @post.origin = "Iraq"
+ def @post.to_param; 325; end
+
+ expected_select = <<-COUNTRIES
+<select id="post_325_origin" name="post[325][origin]"><option value="Afghanistan">Afghanistan</option>
+<option value="Aland Islands">Aland Islands</option>
+<option value="Albania">Albania</option>
+<option value="Algeria">Algeria</option>
+<option value="American Samoa">American Samoa</option>
+<option value="Andorra">Andorra</option>
+<option value="Angola">Angola</option>
+<option value="Anguilla">Anguilla</option>
+<option value="Antarctica">Antarctica</option>
+<option value="Antigua And Barbuda">Antigua And Barbuda</option>
+<option value="Argentina">Argentina</option>
+<option value="Armenia">Armenia</option>
+<option value="Aruba">Aruba</option>
+<option value="Australia">Australia</option>
+<option value="Austria">Austria</option>
+<option value="Azerbaijan">Azerbaijan</option>
+<option value="Bahamas">Bahamas</option>
+<option value="Bahrain">Bahrain</option>
+<option value="Bangladesh">Bangladesh</option>
+<option value="Barbados">Barbados</option>
+<option value="Belarus">Belarus</option>
+<option value="Belgium">Belgium</option>
+<option value="Belize">Belize</option>
+<option value="Benin">Benin</option>
+<option value="Bermuda">Bermuda</option>
+<option value="Bhutan">Bhutan</option>
+<option value="Bolivia">Bolivia</option>
+<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
+<option value="Botswana">Botswana</option>
+<option value="Bouvet Island">Bouvet Island</option>
+<option value="Brazil">Brazil</option>
+<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
+<option value="Brunei Darussalam">Brunei Darussalam</option>
+<option value="Bulgaria">Bulgaria</option>
+<option value="Burkina Faso">Burkina Faso</option>
+<option value="Burundi">Burundi</option>
+<option value="Cambodia">Cambodia</option>
+<option value="Cameroon">Cameroon</option>
+<option value="Canada">Canada</option>
+<option value="Cape Verde">Cape Verde</option>
+<option value="Cayman Islands">Cayman Islands</option>
+<option value="Central African Republic">Central African Republic</option>
+<option value="Chad">Chad</option>
+<option value="Chile">Chile</option>
+<option value="China">China</option>
+<option value="Christmas Island">Christmas Island</option>
+<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
+<option value="Colombia">Colombia</option>
+<option value="Comoros">Comoros</option>
+<option value="Congo">Congo</option>
+<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
+<option value="Cook Islands">Cook Islands</option>
+<option value="Costa Rica">Costa Rica</option>
+<option value="Cote d'Ivoire">Cote d'Ivoire</option>
+<option value="Croatia">Croatia</option>
+<option value="Cuba">Cuba</option>
+<option value="Cyprus">Cyprus</option>
+<option value="Czech Republic">Czech Republic</option>
+<option value="Denmark">Denmark</option>
+<option value="Djibouti">Djibouti</option>
+<option value="Dominica">Dominica</option>
+<option value="Dominican Republic">Dominican Republic</option>
+<option value="Ecuador">Ecuador</option>
+<option value="Egypt">Egypt</option>
+<option value="El Salvador">El Salvador</option>
+<option value="Equatorial Guinea">Equatorial Guinea</option>
+<option value="Eritrea">Eritrea</option>
+<option value="Estonia">Estonia</option>
+<option value="Ethiopia">Ethiopia</option>
+<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
+<option value="Faroe Islands">Faroe Islands</option>
+<option value="Fiji">Fiji</option>
+<option value="Finland">Finland</option>
+<option value="France">France</option>
+<option value="French Guiana">French Guiana</option>
+<option value="French Polynesia">French Polynesia</option>
+<option value="French Southern Territories">French Southern Territories</option>
+<option value="Gabon">Gabon</option>
+<option value="Gambia">Gambia</option>
+<option value="Georgia">Georgia</option>
+<option value="Germany">Germany</option>
+<option value="Ghana">Ghana</option>
+<option value="Gibraltar">Gibraltar</option>
+<option value="Greece">Greece</option>
+<option value="Greenland">Greenland</option>
+<option value="Grenada">Grenada</option>
+<option value="Guadeloupe">Guadeloupe</option>
+<option value="Guam">Guam</option>
+<option value="Guatemala">Guatemala</option>
+<option value="Guernsey">Guernsey</option>
+<option value="Guinea">Guinea</option>
+<option value="Guinea-Bissau">Guinea-Bissau</option>
+<option value="Guyana">Guyana</option>
+<option value="Haiti">Haiti</option>
+<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
+<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
+<option value="Honduras">Honduras</option>
+<option value="Hong Kong">Hong Kong</option>
+<option value="Hungary">Hungary</option>
+<option value="Iceland">Iceland</option>
+<option value="India">India</option>
+<option value="Indonesia">Indonesia</option>
+<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
+<option selected="selected" value="Iraq">Iraq</option>
+<option value="Ireland">Ireland</option>
+<option value="Isle of Man">Isle of Man</option>
+<option value="Israel">Israel</option>
+<option value="Italy">Italy</option>
+<option value="Jamaica">Jamaica</option>
+<option value="Japan">Japan</option>
+<option value="Jersey">Jersey</option>
+<option value="Jordan">Jordan</option>
+<option value="Kazakhstan">Kazakhstan</option>
+<option value="Kenya">Kenya</option>
+<option value="Kiribati">Kiribati</option>
+<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
+<option value="Korea, Republic of">Korea, Republic of</option>
+<option value="Kuwait">Kuwait</option>
+<option value="Kyrgyzstan">Kyrgyzstan</option>
+<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
+<option value="Latvia">Latvia</option>
+<option value="Lebanon">Lebanon</option>
+<option value="Lesotho">Lesotho</option>
+<option value="Liberia">Liberia</option>
+<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
+<option value="Liechtenstein">Liechtenstein</option>
+<option value="Lithuania">Lithuania</option>
+<option value="Luxembourg">Luxembourg</option>
+<option value="Macao">Macao</option>
+<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
+<option value="Madagascar">Madagascar</option>
+<option value="Malawi">Malawi</option>
+<option value="Malaysia">Malaysia</option>
+<option value="Maldives">Maldives</option>
+<option value="Mali">Mali</option>
+<option value="Malta">Malta</option>
+<option value="Marshall Islands">Marshall Islands</option>
+<option value="Martinique">Martinique</option>
+<option value="Mauritania">Mauritania</option>
+<option value="Mauritius">Mauritius</option>
+<option value="Mayotte">Mayotte</option>
+<option value="Mexico">Mexico</option>
+<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
+<option value="Moldova, Republic of">Moldova, Republic of</option>
+<option value="Monaco">Monaco</option>
+<option value="Mongolia">Mongolia</option>
+<option value="Montenegro">Montenegro</option>
+<option value="Montserrat">Montserrat</option>
+<option value="Morocco">Morocco</option>
+<option value="Mozambique">Mozambique</option>
+<option value="Myanmar">Myanmar</option>
+<option value="Namibia">Namibia</option>
+<option value="Nauru">Nauru</option>
+<option value="Nepal">Nepal</option>
+<option value="Netherlands">Netherlands</option>
+<option value="Netherlands Antilles">Netherlands Antilles</option>
+<option value="New Caledonia">New Caledonia</option>
+<option value="New Zealand">New Zealand</option>
+<option value="Nicaragua">Nicaragua</option>
+<option value="Niger">Niger</option>
+<option value="Nigeria">Nigeria</option>
+<option value="Niue">Niue</option>
+<option value="Norfolk Island">Norfolk Island</option>
+<option value="Northern Mariana Islands">Northern Mariana Islands</option>
+<option value="Norway">Norway</option>
+<option value="Oman">Oman</option>
+<option value="Pakistan">Pakistan</option>
+<option value="Palau">Palau</option>
+<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
+<option value="Panama">Panama</option>
+<option value="Papua New Guinea">Papua New Guinea</option>
+<option value="Paraguay">Paraguay</option>
+<option value="Peru">Peru</option>
+<option value="Philippines">Philippines</option>
+<option value="Pitcairn">Pitcairn</option>
+<option value="Poland">Poland</option>
+<option value="Portugal">Portugal</option>
+<option value="Puerto Rico">Puerto Rico</option>
+<option value="Qatar">Qatar</option>
+<option value="Reunion">Reunion</option>
+<option value="Romania">Romania</option>
+<option value="Russian Federation">Russian Federation</option>
+<option value="Rwanda">Rwanda</option>
+<option value="Saint Barthelemy">Saint Barthelemy</option>
+<option value="Saint Helena">Saint Helena</option>
+<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
+<option value="Saint Lucia">Saint Lucia</option>
+<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
+<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
+<option value="Samoa">Samoa</option>
+<option value="San Marino">San Marino</option>
+<option value="Sao Tome and Principe">Sao Tome and Principe</option>
+<option value="Saudi Arabia">Saudi Arabia</option>
+<option value="Senegal">Senegal</option>
+<option value="Serbia">Serbia</option>
+<option value="Seychelles">Seychelles</option>
+<option value="Sierra Leone">Sierra Leone</option>
+<option value="Singapore">Singapore</option>
+<option value="Slovakia">Slovakia</option>
+<option value="Slovenia">Slovenia</option>
+<option value="Solomon Islands">Solomon Islands</option>
+<option value="Somalia">Somalia</option>
+<option value="South Africa">South Africa</option>
+<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
+<option value="Spain">Spain</option>
+<option value="Sri Lanka">Sri Lanka</option>
+<option value="Sudan">Sudan</option>
+<option value="Suriname">Suriname</option>
+<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
+<option value="Swaziland">Swaziland</option>
+<option value="Sweden">Sweden</option>
+<option value="Switzerland">Switzerland</option>
+<option value="Syrian Arab Republic">Syrian Arab Republic</option>
+<option value="Taiwan, Province of China">Taiwan, Province of China</option>
+<option value="Tajikistan">Tajikistan</option>
+<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
+<option value="Thailand">Thailand</option>
+<option value="Timor-Leste">Timor-Leste</option>
+<option value="Togo">Togo</option>
+<option value="Tokelau">Tokelau</option>
+<option value="Tonga">Tonga</option>
+<option value="Trinidad and Tobago">Trinidad and Tobago</option>
+<option value="Tunisia">Tunisia</option>
+<option value="Turkey">Turkey</option>
+<option value="Turkmenistan">Turkmenistan</option>
+<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
+<option value="Tuvalu">Tuvalu</option>
+<option value="Uganda">Uganda</option>
+<option value="Ukraine">Ukraine</option>
+<option value="United Arab Emirates">United Arab Emirates</option>
+<option value="United Kingdom">United Kingdom</option>
+<option value="United States">United States</option>
+<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
+<option value="Uruguay">Uruguay</option>
+<option value="Uzbekistan">Uzbekistan</option>
+<option value="Vanuatu">Vanuatu</option>
+<option value="Venezuela">Venezuela</option>
+<option value="Viet Nam">Viet Nam</option>
+<option value="Virgin Islands, British">Virgin Islands, British</option>
+<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
+<option value="Wallis and Futuna">Wallis and Futuna</option>
+<option value="Western Sahara">Western Sahara</option>
+<option value="Yemen">Yemen</option>
+<option value="Zambia">Zambia</option>
+<option value="Zimbabwe">Zimbabwe</option></select>
+COUNTRIES
+
+ fields_for "post[]", @post do |f|
+ concat f.country_select("origin")
+ end
+
+ assert_dom_equal(expected_select[0..-2], output_buffer)
+ end
+
+end \ No newline at end of file
diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb
index 39649c3622..52e8bf376a 100644
--- a/actionpack/test/template/form_helper_test.rb
+++ b/actionpack/test/template/form_helper_test.rb
@@ -6,7 +6,7 @@ silence_warnings do
alias_method :title_before_type_cast, :title unless respond_to?(:title_before_type_cast)
alias_method :body_before_type_cast, :body unless respond_to?(:body_before_type_cast)
alias_method :author_name_before_type_cast, :author_name unless respond_to?(:author_name_before_type_cast)
- alias_method :secret?, :secret
+ alias_method :secret?, :secret
def new_record=(boolean)
@new_record = boolean
@@ -22,6 +22,7 @@ silence_warnings do
attr_reader :post_id
def save; @id = 1; @post_id = 1 end
def new_record?; @id.nil? end
+ def to_param; @id; end
def name
@id.nil? ? 'new comment' : "comment ##{@id}"
end
@@ -30,7 +31,6 @@ end
class Comment::Nested < Comment; end
-
class FormHelperTest < ActionView::TestCase
tests ActionView::Helpers::FormHelper
@@ -447,6 +447,117 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_nested_fields_for_with_nested_collections
+ form_for('post[]', @post) do |f|
+ concat f.text_field(:title)
+ f.fields_for('comment[]', @comment) do |c|
+ concat c.text_field(:name)
+ end
+ end
+
+ expected = "<form action='http://www.example.com' method='post'>" +
+ "<input name='post[123][title]' size='30' type='text' id='post_123_title' value='Hello World' />" +
+ "<input name='post[123][comment][][name]' size='30' type='text' id='post_123_comment__name' value='new comment' />" +
+ "</form>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_index
+ form_for('post', @post, :index => 1) do |c|
+ concat c.text_field(:title)
+ c.fields_for('comment', @comment, :index => 1) do |r|
+ concat r.text_field(:name)
+ end
+ end
+
+ expected = "<form action='http://www.example.com' method='post'>" +
+ "<input name='post[1][title]' size='30' type='text' id='post_1_title' value='Hello World' />" +
+ "<input name='post[1][comment][1][name]' size='30' type='text' id='post_1_comment_1_name' value='new comment' />" +
+ "</form>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_index
+ form_for(:post, @post, :index => 1) do |f|
+ f.fields_for(:comment, @post) do |c|
+ concat c.text_field(:title)
+ end
+ end
+
+ expected = "<form action='http://www.example.com' method='post'>" +
+ "<input name='post[1][comment][title]' size='30' type='text' id='post_1_comment_title' value='Hello World' />" +
+ "</form>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_index_on_both
+ form_for(:post, @post, :index => 1) do |f|
+ f.fields_for(:comment, @post, :index => 5) do |c|
+ concat c.text_field(:title)
+ end
+ end
+
+ expected = "<form action='http://www.example.com' method='post'>" +
+ "<input name='post[1][comment][5][title]' size='30' type='text' id='post_1_comment_5_title' value='Hello World' />" +
+ "</form>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_auto_index
+ form_for("post[]", @post) do |f|
+ f.fields_for(:comment, @post) do |c|
+ concat c.text_field(:title)
+ end
+ end
+
+ expected = "<form action='http://www.example.com' method='post'>" +
+ "<input name='post[123][comment][title]' size='30' type='text' id='post_123_comment_title' value='Hello World' />" +
+ "</form>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_auto_index_on_both
+ form_for("post[]", @post) do |f|
+ f.fields_for("comment[]", @post) do |c|
+ concat c.text_field(:title)
+ end
+ end
+
+ expected = "<form action='http://www.example.com' method='post'>" +
+ "<input name='post[123][comment][123][title]' size='30' type='text' id='post_123_comment_123_title' value='Hello World' />" +
+ "</form>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_index_and_auto_index
+ form_for("post[]", @post) do |f|
+ f.fields_for(:comment, @post, :index => 5) do |c|
+ concat c.text_field(:title)
+ end
+ end
+
+ form_for(:post, @post, :index => 1) do |f|
+ f.fields_for("comment[]", @post) do |c|
+ concat c.text_field(:title)
+ end
+ end
+
+ expected = "<form action='http://www.example.com' method='post'>" +
+ "<input name='post[123][comment][5][title]' size='30' type='text' id='post_123_comment_5_title' value='Hello World' />" +
+ "</form>" +
+ "<form action='http://www.example.com' method='post'>" +
+ "<input name='post[1][comment][123][title]' size='30' type='text' id='post_1_comment_123_title' value='Hello World' />" +
+ "</form>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
def test_fields_for
fields_for(:post, @post) do |f|
concat f.text_field(:title)
@@ -831,7 +942,6 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
-
protected
def comments_path(post)
"/posts/#{post.id}/comments"
diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb
index 9dd43d7b4f..a33eb85b66 100644
--- a/actionpack/test/template/form_options_helper_test.rb
+++ b/actionpack/test/template/form_options_helper_test.rb
@@ -472,1545 +472,6 @@ uses_mocha "FormOptionsHelperTest" do
assert_dom_equal expected, collection_select("post", "author_name", @posts, "author_name", "author_name", { :include_blank => true, :name => 'post[author_name][]' }, :multiple => true)
end
- def test_country_select
- @post = Post.new
- @post.origin = "Denmark"
- expected_select = <<-COUNTRIES
-<select id="post_origin" name="post[origin]"><option value="Afghanistan">Afghanistan</option>
-<option value="Aland Islands">Aland Islands</option>
-<option value="Albania">Albania</option>
-<option value="Algeria">Algeria</option>
-<option value="American Samoa">American Samoa</option>
-<option value="Andorra">Andorra</option>
-<option value="Angola">Angola</option>
-<option value="Anguilla">Anguilla</option>
-<option value="Antarctica">Antarctica</option>
-<option value="Antigua And Barbuda">Antigua And Barbuda</option>
-<option value="Argentina">Argentina</option>
-<option value="Armenia">Armenia</option>
-<option value="Aruba">Aruba</option>
-<option value="Australia">Australia</option>
-<option value="Austria">Austria</option>
-<option value="Azerbaijan">Azerbaijan</option>
-<option value="Bahamas">Bahamas</option>
-<option value="Bahrain">Bahrain</option>
-<option value="Bangladesh">Bangladesh</option>
-<option value="Barbados">Barbados</option>
-<option value="Belarus">Belarus</option>
-<option value="Belgium">Belgium</option>
-<option value="Belize">Belize</option>
-<option value="Benin">Benin</option>
-<option value="Bermuda">Bermuda</option>
-<option value="Bhutan">Bhutan</option>
-<option value="Bolivia">Bolivia</option>
-<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
-<option value="Botswana">Botswana</option>
-<option value="Bouvet Island">Bouvet Island</option>
-<option value="Brazil">Brazil</option>
-<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
-<option value="Brunei Darussalam">Brunei Darussalam</option>
-<option value="Bulgaria">Bulgaria</option>
-<option value="Burkina Faso">Burkina Faso</option>
-<option value="Burundi">Burundi</option>
-<option value="Cambodia">Cambodia</option>
-<option value="Cameroon">Cameroon</option>
-<option value="Canada">Canada</option>
-<option value="Cape Verde">Cape Verde</option>
-<option value="Cayman Islands">Cayman Islands</option>
-<option value="Central African Republic">Central African Republic</option>
-<option value="Chad">Chad</option>
-<option value="Chile">Chile</option>
-<option value="China">China</option>
-<option value="Christmas Island">Christmas Island</option>
-<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
-<option value="Colombia">Colombia</option>
-<option value="Comoros">Comoros</option>
-<option value="Congo">Congo</option>
-<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
-<option value="Cook Islands">Cook Islands</option>
-<option value="Costa Rica">Costa Rica</option>
-<option value="Cote d'Ivoire">Cote d'Ivoire</option>
-<option value="Croatia">Croatia</option>
-<option value="Cuba">Cuba</option>
-<option value="Cyprus">Cyprus</option>
-<option value="Czech Republic">Czech Republic</option>
-<option selected="selected" value="Denmark">Denmark</option>
-<option value="Djibouti">Djibouti</option>
-<option value="Dominica">Dominica</option>
-<option value="Dominican Republic">Dominican Republic</option>
-<option value="Ecuador">Ecuador</option>
-<option value="Egypt">Egypt</option>
-<option value="El Salvador">El Salvador</option>
-<option value="Equatorial Guinea">Equatorial Guinea</option>
-<option value="Eritrea">Eritrea</option>
-<option value="Estonia">Estonia</option>
-<option value="Ethiopia">Ethiopia</option>
-<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
-<option value="Faroe Islands">Faroe Islands</option>
-<option value="Fiji">Fiji</option>
-<option value="Finland">Finland</option>
-<option value="France">France</option>
-<option value="French Guiana">French Guiana</option>
-<option value="French Polynesia">French Polynesia</option>
-<option value="French Southern Territories">French Southern Territories</option>
-<option value="Gabon">Gabon</option>
-<option value="Gambia">Gambia</option>
-<option value="Georgia">Georgia</option>
-<option value="Germany">Germany</option>
-<option value="Ghana">Ghana</option>
-<option value="Gibraltar">Gibraltar</option>
-<option value="Greece">Greece</option>
-<option value="Greenland">Greenland</option>
-<option value="Grenada">Grenada</option>
-<option value="Guadeloupe">Guadeloupe</option>
-<option value="Guam">Guam</option>
-<option value="Guatemala">Guatemala</option>
-<option value="Guernsey">Guernsey</option>
-<option value="Guinea">Guinea</option>
-<option value="Guinea-Bissau">Guinea-Bissau</option>
-<option value="Guyana">Guyana</option>
-<option value="Haiti">Haiti</option>
-<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
-<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
-<option value="Honduras">Honduras</option>
-<option value="Hong Kong">Hong Kong</option>
-<option value="Hungary">Hungary</option>
-<option value="Iceland">Iceland</option>
-<option value="India">India</option>
-<option value="Indonesia">Indonesia</option>
-<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
-<option value="Iraq">Iraq</option>
-<option value="Ireland">Ireland</option>
-<option value="Isle of Man">Isle of Man</option>
-<option value="Israel">Israel</option>
-<option value="Italy">Italy</option>
-<option value="Jamaica">Jamaica</option>
-<option value="Japan">Japan</option>
-<option value="Jersey">Jersey</option>
-<option value="Jordan">Jordan</option>
-<option value="Kazakhstan">Kazakhstan</option>
-<option value="Kenya">Kenya</option>
-<option value="Kiribati">Kiribati</option>
-<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
-<option value="Korea, Republic of">Korea, Republic of</option>
-<option value="Kuwait">Kuwait</option>
-<option value="Kyrgyzstan">Kyrgyzstan</option>
-<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
-<option value="Latvia">Latvia</option>
-<option value="Lebanon">Lebanon</option>
-<option value="Lesotho">Lesotho</option>
-<option value="Liberia">Liberia</option>
-<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
-<option value="Liechtenstein">Liechtenstein</option>
-<option value="Lithuania">Lithuania</option>
-<option value="Luxembourg">Luxembourg</option>
-<option value="Macao">Macao</option>
-<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
-<option value="Madagascar">Madagascar</option>
-<option value="Malawi">Malawi</option>
-<option value="Malaysia">Malaysia</option>
-<option value="Maldives">Maldives</option>
-<option value="Mali">Mali</option>
-<option value="Malta">Malta</option>
-<option value="Marshall Islands">Marshall Islands</option>
-<option value="Martinique">Martinique</option>
-<option value="Mauritania">Mauritania</option>
-<option value="Mauritius">Mauritius</option>
-<option value="Mayotte">Mayotte</option>
-<option value="Mexico">Mexico</option>
-<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
-<option value="Moldova, Republic of">Moldova, Republic of</option>
-<option value="Monaco">Monaco</option>
-<option value="Mongolia">Mongolia</option>
-<option value="Montenegro">Montenegro</option>
-<option value="Montserrat">Montserrat</option>
-<option value="Morocco">Morocco</option>
-<option value="Mozambique">Mozambique</option>
-<option value="Myanmar">Myanmar</option>
-<option value="Namibia">Namibia</option>
-<option value="Nauru">Nauru</option>
-<option value="Nepal">Nepal</option>
-<option value="Netherlands">Netherlands</option>
-<option value="Netherlands Antilles">Netherlands Antilles</option>
-<option value="New Caledonia">New Caledonia</option>
-<option value="New Zealand">New Zealand</option>
-<option value="Nicaragua">Nicaragua</option>
-<option value="Niger">Niger</option>
-<option value="Nigeria">Nigeria</option>
-<option value="Niue">Niue</option>
-<option value="Norfolk Island">Norfolk Island</option>
-<option value="Northern Mariana Islands">Northern Mariana Islands</option>
-<option value="Norway">Norway</option>
-<option value="Oman">Oman</option>
-<option value="Pakistan">Pakistan</option>
-<option value="Palau">Palau</option>
-<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
-<option value="Panama">Panama</option>
-<option value="Papua New Guinea">Papua New Guinea</option>
-<option value="Paraguay">Paraguay</option>
-<option value="Peru">Peru</option>
-<option value="Philippines">Philippines</option>
-<option value="Pitcairn">Pitcairn</option>
-<option value="Poland">Poland</option>
-<option value="Portugal">Portugal</option>
-<option value="Puerto Rico">Puerto Rico</option>
-<option value="Qatar">Qatar</option>
-<option value="Reunion">Reunion</option>
-<option value="Romania">Romania</option>
-<option value="Russian Federation">Russian Federation</option>
-<option value="Rwanda">Rwanda</option>
-<option value="Saint Barthelemy">Saint Barthelemy</option>
-<option value="Saint Helena">Saint Helena</option>
-<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
-<option value="Saint Lucia">Saint Lucia</option>
-<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
-<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
-<option value="Samoa">Samoa</option>
-<option value="San Marino">San Marino</option>
-<option value="Sao Tome and Principe">Sao Tome and Principe</option>
-<option value="Saudi Arabia">Saudi Arabia</option>
-<option value="Senegal">Senegal</option>
-<option value="Serbia">Serbia</option>
-<option value="Seychelles">Seychelles</option>
-<option value="Sierra Leone">Sierra Leone</option>
-<option value="Singapore">Singapore</option>
-<option value="Slovakia">Slovakia</option>
-<option value="Slovenia">Slovenia</option>
-<option value="Solomon Islands">Solomon Islands</option>
-<option value="Somalia">Somalia</option>
-<option value="South Africa">South Africa</option>
-<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
-<option value="Spain">Spain</option>
-<option value="Sri Lanka">Sri Lanka</option>
-<option value="Sudan">Sudan</option>
-<option value="Suriname">Suriname</option>
-<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
-<option value="Swaziland">Swaziland</option>
-<option value="Sweden">Sweden</option>
-<option value="Switzerland">Switzerland</option>
-<option value="Syrian Arab Republic">Syrian Arab Republic</option>
-<option value="Taiwan, Province of China">Taiwan, Province of China</option>
-<option value="Tajikistan">Tajikistan</option>
-<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
-<option value="Thailand">Thailand</option>
-<option value="Timor-Leste">Timor-Leste</option>
-<option value="Togo">Togo</option>
-<option value="Tokelau">Tokelau</option>
-<option value="Tonga">Tonga</option>
-<option value="Trinidad and Tobago">Trinidad and Tobago</option>
-<option value="Tunisia">Tunisia</option>
-<option value="Turkey">Turkey</option>
-<option value="Turkmenistan">Turkmenistan</option>
-<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
-<option value="Tuvalu">Tuvalu</option>
-<option value="Uganda">Uganda</option>
-<option value="Ukraine">Ukraine</option>
-<option value="United Arab Emirates">United Arab Emirates</option>
-<option value="United Kingdom">United Kingdom</option>
-<option value="United States">United States</option>
-<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
-<option value="Uruguay">Uruguay</option>
-<option value="Uzbekistan">Uzbekistan</option>
-<option value="Vanuatu">Vanuatu</option>
-<option value="Venezuela">Venezuela</option>
-<option value="Viet Nam">Viet Nam</option>
-<option value="Virgin Islands, British">Virgin Islands, British</option>
-<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
-<option value="Wallis and Futuna">Wallis and Futuna</option>
-<option value="Western Sahara">Western Sahara</option>
-<option value="Yemen">Yemen</option>
-<option value="Zambia">Zambia</option>
-<option value="Zimbabwe">Zimbabwe</option></select>
- COUNTRIES
- assert_dom_equal(expected_select[0..-2], country_select("post", "origin"))
- end
-
- def test_country_select_with_priority_countries
- @post = Post.new
- @post.origin = "Denmark"
- expected_select = <<-COUNTRIES
-<select id="post_origin" name="post[origin]"><option value="New Zealand">New Zealand</option>
-<option value="Nicaragua">Nicaragua</option><option value="" disabled="disabled">-------------</option>
-<option value="Afghanistan">Afghanistan</option>
-<option value="Aland Islands">Aland Islands</option>
-<option value="Albania">Albania</option>
-<option value="Algeria">Algeria</option>
-<option value="American Samoa">American Samoa</option>
-<option value="Andorra">Andorra</option>
-<option value="Angola">Angola</option>
-<option value="Anguilla">Anguilla</option>
-<option value="Antarctica">Antarctica</option>
-<option value="Antigua And Barbuda">Antigua And Barbuda</option>
-<option value="Argentina">Argentina</option>
-<option value="Armenia">Armenia</option>
-<option value="Aruba">Aruba</option>
-<option value="Australia">Australia</option>
-<option value="Austria">Austria</option>
-<option value="Azerbaijan">Azerbaijan</option>
-<option value="Bahamas">Bahamas</option>
-<option value="Bahrain">Bahrain</option>
-<option value="Bangladesh">Bangladesh</option>
-<option value="Barbados">Barbados</option>
-<option value="Belarus">Belarus</option>
-<option value="Belgium">Belgium</option>
-<option value="Belize">Belize</option>
-<option value="Benin">Benin</option>
-<option value="Bermuda">Bermuda</option>
-<option value="Bhutan">Bhutan</option>
-<option value="Bolivia">Bolivia</option>
-<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
-<option value="Botswana">Botswana</option>
-<option value="Bouvet Island">Bouvet Island</option>
-<option value="Brazil">Brazil</option>
-<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
-<option value="Brunei Darussalam">Brunei Darussalam</option>
-<option value="Bulgaria">Bulgaria</option>
-<option value="Burkina Faso">Burkina Faso</option>
-<option value="Burundi">Burundi</option>
-<option value="Cambodia">Cambodia</option>
-<option value="Cameroon">Cameroon</option>
-<option value="Canada">Canada</option>
-<option value="Cape Verde">Cape Verde</option>
-<option value="Cayman Islands">Cayman Islands</option>
-<option value="Central African Republic">Central African Republic</option>
-<option value="Chad">Chad</option>
-<option value="Chile">Chile</option>
-<option value="China">China</option>
-<option value="Christmas Island">Christmas Island</option>
-<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
-<option value="Colombia">Colombia</option>
-<option value="Comoros">Comoros</option>
-<option value="Congo">Congo</option>
-<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
-<option value="Cook Islands">Cook Islands</option>
-<option value="Costa Rica">Costa Rica</option>
-<option value="Cote d'Ivoire">Cote d'Ivoire</option>
-<option value="Croatia">Croatia</option>
-<option value="Cuba">Cuba</option>
-<option value="Cyprus">Cyprus</option>
-<option value="Czech Republic">Czech Republic</option>
-<option selected="selected" value="Denmark">Denmark</option>
-<option value="Djibouti">Djibouti</option>
-<option value="Dominica">Dominica</option>
-<option value="Dominican Republic">Dominican Republic</option>
-<option value="Ecuador">Ecuador</option>
-<option value="Egypt">Egypt</option>
-<option value="El Salvador">El Salvador</option>
-<option value="Equatorial Guinea">Equatorial Guinea</option>
-<option value="Eritrea">Eritrea</option>
-<option value="Estonia">Estonia</option>
-<option value="Ethiopia">Ethiopia</option>
-<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
-<option value="Faroe Islands">Faroe Islands</option>
-<option value="Fiji">Fiji</option>
-<option value="Finland">Finland</option>
-<option value="France">France</option>
-<option value="French Guiana">French Guiana</option>
-<option value="French Polynesia">French Polynesia</option>
-<option value="French Southern Territories">French Southern Territories</option>
-<option value="Gabon">Gabon</option>
-<option value="Gambia">Gambia</option>
-<option value="Georgia">Georgia</option>
-<option value="Germany">Germany</option>
-<option value="Ghana">Ghana</option>
-<option value="Gibraltar">Gibraltar</option>
-<option value="Greece">Greece</option>
-<option value="Greenland">Greenland</option>
-<option value="Grenada">Grenada</option>
-<option value="Guadeloupe">Guadeloupe</option>
-<option value="Guam">Guam</option>
-<option value="Guatemala">Guatemala</option>
-<option value="Guernsey">Guernsey</option>
-<option value="Guinea">Guinea</option>
-<option value="Guinea-Bissau">Guinea-Bissau</option>
-<option value="Guyana">Guyana</option>
-<option value="Haiti">Haiti</option>
-<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
-<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
-<option value="Honduras">Honduras</option>
-<option value="Hong Kong">Hong Kong</option>
-<option value="Hungary">Hungary</option>
-<option value="Iceland">Iceland</option>
-<option value="India">India</option>
-<option value="Indonesia">Indonesia</option>
-<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
-<option value="Iraq">Iraq</option>
-<option value="Ireland">Ireland</option>
-<option value="Isle of Man">Isle of Man</option>
-<option value="Israel">Israel</option>
-<option value="Italy">Italy</option>
-<option value="Jamaica">Jamaica</option>
-<option value="Japan">Japan</option>
-<option value="Jersey">Jersey</option>
-<option value="Jordan">Jordan</option>
-<option value="Kazakhstan">Kazakhstan</option>
-<option value="Kenya">Kenya</option>
-<option value="Kiribati">Kiribati</option>
-<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
-<option value="Korea, Republic of">Korea, Republic of</option>
-<option value="Kuwait">Kuwait</option>
-<option value="Kyrgyzstan">Kyrgyzstan</option>
-<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
-<option value="Latvia">Latvia</option>
-<option value="Lebanon">Lebanon</option>
-<option value="Lesotho">Lesotho</option>
-<option value="Liberia">Liberia</option>
-<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
-<option value="Liechtenstein">Liechtenstein</option>
-<option value="Lithuania">Lithuania</option>
-<option value="Luxembourg">Luxembourg</option>
-<option value="Macao">Macao</option>
-<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
-<option value="Madagascar">Madagascar</option>
-<option value="Malawi">Malawi</option>
-<option value="Malaysia">Malaysia</option>
-<option value="Maldives">Maldives</option>
-<option value="Mali">Mali</option>
-<option value="Malta">Malta</option>
-<option value="Marshall Islands">Marshall Islands</option>
-<option value="Martinique">Martinique</option>
-<option value="Mauritania">Mauritania</option>
-<option value="Mauritius">Mauritius</option>
-<option value="Mayotte">Mayotte</option>
-<option value="Mexico">Mexico</option>
-<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
-<option value="Moldova, Republic of">Moldova, Republic of</option>
-<option value="Monaco">Monaco</option>
-<option value="Mongolia">Mongolia</option>
-<option value="Montenegro">Montenegro</option>
-<option value="Montserrat">Montserrat</option>
-<option value="Morocco">Morocco</option>
-<option value="Mozambique">Mozambique</option>
-<option value="Myanmar">Myanmar</option>
-<option value="Namibia">Namibia</option>
-<option value="Nauru">Nauru</option>
-<option value="Nepal">Nepal</option>
-<option value="Netherlands">Netherlands</option>
-<option value="Netherlands Antilles">Netherlands Antilles</option>
-<option value="New Caledonia">New Caledonia</option>
-<option value="New Zealand">New Zealand</option>
-<option value="Nicaragua">Nicaragua</option>
-<option value="Niger">Niger</option>
-<option value="Nigeria">Nigeria</option>
-<option value="Niue">Niue</option>
-<option value="Norfolk Island">Norfolk Island</option>
-<option value="Northern Mariana Islands">Northern Mariana Islands</option>
-<option value="Norway">Norway</option>
-<option value="Oman">Oman</option>
-<option value="Pakistan">Pakistan</option>
-<option value="Palau">Palau</option>
-<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
-<option value="Panama">Panama</option>
-<option value="Papua New Guinea">Papua New Guinea</option>
-<option value="Paraguay">Paraguay</option>
-<option value="Peru">Peru</option>
-<option value="Philippines">Philippines</option>
-<option value="Pitcairn">Pitcairn</option>
-<option value="Poland">Poland</option>
-<option value="Portugal">Portugal</option>
-<option value="Puerto Rico">Puerto Rico</option>
-<option value="Qatar">Qatar</option>
-<option value="Reunion">Reunion</option>
-<option value="Romania">Romania</option>
-<option value="Russian Federation">Russian Federation</option>
-<option value="Rwanda">Rwanda</option>
-<option value="Saint Barthelemy">Saint Barthelemy</option>
-<option value="Saint Helena">Saint Helena</option>
-<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
-<option value="Saint Lucia">Saint Lucia</option>
-<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
-<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
-<option value="Samoa">Samoa</option>
-<option value="San Marino">San Marino</option>
-<option value="Sao Tome and Principe">Sao Tome and Principe</option>
-<option value="Saudi Arabia">Saudi Arabia</option>
-<option value="Senegal">Senegal</option>
-<option value="Serbia">Serbia</option>
-<option value="Seychelles">Seychelles</option>
-<option value="Sierra Leone">Sierra Leone</option>
-<option value="Singapore">Singapore</option>
-<option value="Slovakia">Slovakia</option>
-<option value="Slovenia">Slovenia</option>
-<option value="Solomon Islands">Solomon Islands</option>
-<option value="Somalia">Somalia</option>
-<option value="South Africa">South Africa</option>
-<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
-<option value="Spain">Spain</option>
-<option value="Sri Lanka">Sri Lanka</option>
-<option value="Sudan">Sudan</option>
-<option value="Suriname">Suriname</option>
-<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
-<option value="Swaziland">Swaziland</option>
-<option value="Sweden">Sweden</option>
-<option value="Switzerland">Switzerland</option>
-<option value="Syrian Arab Republic">Syrian Arab Republic</option>
-<option value="Taiwan, Province of China">Taiwan, Province of China</option>
-<option value="Tajikistan">Tajikistan</option>
-<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
-<option value="Thailand">Thailand</option>
-<option value="Timor-Leste">Timor-Leste</option>
-<option value="Togo">Togo</option>
-<option value="Tokelau">Tokelau</option>
-<option value="Tonga">Tonga</option>
-<option value="Trinidad and Tobago">Trinidad and Tobago</option>
-<option value="Tunisia">Tunisia</option>
-<option value="Turkey">Turkey</option>
-<option value="Turkmenistan">Turkmenistan</option>
-<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
-<option value="Tuvalu">Tuvalu</option>
-<option value="Uganda">Uganda</option>
-<option value="Ukraine">Ukraine</option>
-<option value="United Arab Emirates">United Arab Emirates</option>
-<option value="United Kingdom">United Kingdom</option>
-<option value="United States">United States</option>
-<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
-<option value="Uruguay">Uruguay</option>
-<option value="Uzbekistan">Uzbekistan</option>
-<option value="Vanuatu">Vanuatu</option>
-<option value="Venezuela">Venezuela</option>
-<option value="Viet Nam">Viet Nam</option>
-<option value="Virgin Islands, British">Virgin Islands, British</option>
-<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
-<option value="Wallis and Futuna">Wallis and Futuna</option>
-<option value="Western Sahara">Western Sahara</option>
-<option value="Yemen">Yemen</option>
-<option value="Zambia">Zambia</option>
-<option value="Zimbabwe">Zimbabwe</option></select>
- COUNTRIES
- assert_dom_equal(expected_select[0..-2], country_select("post", "origin", ["New Zealand", "Nicaragua"]))
- end
-
- def test_country_select_with_selected_priority_country
- @post = Post.new
- @post.origin = "New Zealand"
- expected_select = <<-COUNTRIES
-<select id="post_origin" name="post[origin]"><option selected="selected" value="New Zealand">New Zealand</option>
-<option value="Nicaragua">Nicaragua</option><option value="" disabled="disabled">-------------</option>
-<option value="Afghanistan">Afghanistan</option>
-<option value="Aland Islands">Aland Islands</option>
-<option value="Albania">Albania</option>
-<option value="Algeria">Algeria</option>
-<option value="American Samoa">American Samoa</option>
-<option value="Andorra">Andorra</option>
-<option value="Angola">Angola</option>
-<option value="Anguilla">Anguilla</option>
-<option value="Antarctica">Antarctica</option>
-<option value="Antigua And Barbuda">Antigua And Barbuda</option>
-<option value="Argentina">Argentina</option>
-<option value="Armenia">Armenia</option>
-<option value="Aruba">Aruba</option>
-<option value="Australia">Australia</option>
-<option value="Austria">Austria</option>
-<option value="Azerbaijan">Azerbaijan</option>
-<option value="Bahamas">Bahamas</option>
-<option value="Bahrain">Bahrain</option>
-<option value="Bangladesh">Bangladesh</option>
-<option value="Barbados">Barbados</option>
-<option value="Belarus">Belarus</option>
-<option value="Belgium">Belgium</option>
-<option value="Belize">Belize</option>
-<option value="Benin">Benin</option>
-<option value="Bermuda">Bermuda</option>
-<option value="Bhutan">Bhutan</option>
-<option value="Bolivia">Bolivia</option>
-<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
-<option value="Botswana">Botswana</option>
-<option value="Bouvet Island">Bouvet Island</option>
-<option value="Brazil">Brazil</option>
-<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
-<option value="Brunei Darussalam">Brunei Darussalam</option>
-<option value="Bulgaria">Bulgaria</option>
-<option value="Burkina Faso">Burkina Faso</option>
-<option value="Burundi">Burundi</option>
-<option value="Cambodia">Cambodia</option>
-<option value="Cameroon">Cameroon</option>
-<option value="Canada">Canada</option>
-<option value="Cape Verde">Cape Verde</option>
-<option value="Cayman Islands">Cayman Islands</option>
-<option value="Central African Republic">Central African Republic</option>
-<option value="Chad">Chad</option>
-<option value="Chile">Chile</option>
-<option value="China">China</option>
-<option value="Christmas Island">Christmas Island</option>
-<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
-<option value="Colombia">Colombia</option>
-<option value="Comoros">Comoros</option>
-<option value="Congo">Congo</option>
-<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
-<option value="Cook Islands">Cook Islands</option>
-<option value="Costa Rica">Costa Rica</option>
-<option value="Cote d'Ivoire">Cote d'Ivoire</option>
-<option value="Croatia">Croatia</option>
-<option value="Cuba">Cuba</option>
-<option value="Cyprus">Cyprus</option>
-<option value="Czech Republic">Czech Republic</option>
-<option value="Denmark">Denmark</option>
-<option value="Djibouti">Djibouti</option>
-<option value="Dominica">Dominica</option>
-<option value="Dominican Republic">Dominican Republic</option>
-<option value="Ecuador">Ecuador</option>
-<option value="Egypt">Egypt</option>
-<option value="El Salvador">El Salvador</option>
-<option value="Equatorial Guinea">Equatorial Guinea</option>
-<option value="Eritrea">Eritrea</option>
-<option value="Estonia">Estonia</option>
-<option value="Ethiopia">Ethiopia</option>
-<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
-<option value="Faroe Islands">Faroe Islands</option>
-<option value="Fiji">Fiji</option>
-<option value="Finland">Finland</option>
-<option value="France">France</option>
-<option value="French Guiana">French Guiana</option>
-<option value="French Polynesia">French Polynesia</option>
-<option value="French Southern Territories">French Southern Territories</option>
-<option value="Gabon">Gabon</option>
-<option value="Gambia">Gambia</option>
-<option value="Georgia">Georgia</option>
-<option value="Germany">Germany</option>
-<option value="Ghana">Ghana</option>
-<option value="Gibraltar">Gibraltar</option>
-<option value="Greece">Greece</option>
-<option value="Greenland">Greenland</option>
-<option value="Grenada">Grenada</option>
-<option value="Guadeloupe">Guadeloupe</option>
-<option value="Guam">Guam</option>
-<option value="Guatemala">Guatemala</option>
-<option value="Guernsey">Guernsey</option>
-<option value="Guinea">Guinea</option>
-<option value="Guinea-Bissau">Guinea-Bissau</option>
-<option value="Guyana">Guyana</option>
-<option value="Haiti">Haiti</option>
-<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
-<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
-<option value="Honduras">Honduras</option>
-<option value="Hong Kong">Hong Kong</option>
-<option value="Hungary">Hungary</option>
-<option value="Iceland">Iceland</option>
-<option value="India">India</option>
-<option value="Indonesia">Indonesia</option>
-<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
-<option value="Iraq">Iraq</option>
-<option value="Ireland">Ireland</option>
-<option value="Isle of Man">Isle of Man</option>
-<option value="Israel">Israel</option>
-<option value="Italy">Italy</option>
-<option value="Jamaica">Jamaica</option>
-<option value="Japan">Japan</option>
-<option value="Jersey">Jersey</option>
-<option value="Jordan">Jordan</option>
-<option value="Kazakhstan">Kazakhstan</option>
-<option value="Kenya">Kenya</option>
-<option value="Kiribati">Kiribati</option>
-<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
-<option value="Korea, Republic of">Korea, Republic of</option>
-<option value="Kuwait">Kuwait</option>
-<option value="Kyrgyzstan">Kyrgyzstan</option>
-<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
-<option value="Latvia">Latvia</option>
-<option value="Lebanon">Lebanon</option>
-<option value="Lesotho">Lesotho</option>
-<option value="Liberia">Liberia</option>
-<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
-<option value="Liechtenstein">Liechtenstein</option>
-<option value="Lithuania">Lithuania</option>
-<option value="Luxembourg">Luxembourg</option>
-<option value="Macao">Macao</option>
-<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
-<option value="Madagascar">Madagascar</option>
-<option value="Malawi">Malawi</option>
-<option value="Malaysia">Malaysia</option>
-<option value="Maldives">Maldives</option>
-<option value="Mali">Mali</option>
-<option value="Malta">Malta</option>
-<option value="Marshall Islands">Marshall Islands</option>
-<option value="Martinique">Martinique</option>
-<option value="Mauritania">Mauritania</option>
-<option value="Mauritius">Mauritius</option>
-<option value="Mayotte">Mayotte</option>
-<option value="Mexico">Mexico</option>
-<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
-<option value="Moldova, Republic of">Moldova, Republic of</option>
-<option value="Monaco">Monaco</option>
-<option value="Mongolia">Mongolia</option>
-<option value="Montenegro">Montenegro</option>
-<option value="Montserrat">Montserrat</option>
-<option value="Morocco">Morocco</option>
-<option value="Mozambique">Mozambique</option>
-<option value="Myanmar">Myanmar</option>
-<option value="Namibia">Namibia</option>
-<option value="Nauru">Nauru</option>
-<option value="Nepal">Nepal</option>
-<option value="Netherlands">Netherlands</option>
-<option value="Netherlands Antilles">Netherlands Antilles</option>
-<option value="New Caledonia">New Caledonia</option>
-<option selected="selected" value="New Zealand">New Zealand</option>
-<option value="Nicaragua">Nicaragua</option>
-<option value="Niger">Niger</option>
-<option value="Nigeria">Nigeria</option>
-<option value="Niue">Niue</option>
-<option value="Norfolk Island">Norfolk Island</option>
-<option value="Northern Mariana Islands">Northern Mariana Islands</option>
-<option value="Norway">Norway</option>
-<option value="Oman">Oman</option>
-<option value="Pakistan">Pakistan</option>
-<option value="Palau">Palau</option>
-<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
-<option value="Panama">Panama</option>
-<option value="Papua New Guinea">Papua New Guinea</option>
-<option value="Paraguay">Paraguay</option>
-<option value="Peru">Peru</option>
-<option value="Philippines">Philippines</option>
-<option value="Pitcairn">Pitcairn</option>
-<option value="Poland">Poland</option>
-<option value="Portugal">Portugal</option>
-<option value="Puerto Rico">Puerto Rico</option>
-<option value="Qatar">Qatar</option>
-<option value="Reunion">Reunion</option>
-<option value="Romania">Romania</option>
-<option value="Russian Federation">Russian Federation</option>
-<option value="Rwanda">Rwanda</option>
-<option value="Saint Barthelemy">Saint Barthelemy</option>
-<option value="Saint Helena">Saint Helena</option>
-<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
-<option value="Saint Lucia">Saint Lucia</option>
-<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
-<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
-<option value="Samoa">Samoa</option>
-<option value="San Marino">San Marino</option>
-<option value="Sao Tome and Principe">Sao Tome and Principe</option>
-<option value="Saudi Arabia">Saudi Arabia</option>
-<option value="Senegal">Senegal</option>
-<option value="Serbia">Serbia</option>
-<option value="Seychelles">Seychelles</option>
-<option value="Sierra Leone">Sierra Leone</option>
-<option value="Singapore">Singapore</option>
-<option value="Slovakia">Slovakia</option>
-<option value="Slovenia">Slovenia</option>
-<option value="Solomon Islands">Solomon Islands</option>
-<option value="Somalia">Somalia</option>
-<option value="South Africa">South Africa</option>
-<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
-<option value="Spain">Spain</option>
-<option value="Sri Lanka">Sri Lanka</option>
-<option value="Sudan">Sudan</option>
-<option value="Suriname">Suriname</option>
-<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
-<option value="Swaziland">Swaziland</option>
-<option value="Sweden">Sweden</option>
-<option value="Switzerland">Switzerland</option>
-<option value="Syrian Arab Republic">Syrian Arab Republic</option>
-<option value="Taiwan, Province of China">Taiwan, Province of China</option>
-<option value="Tajikistan">Tajikistan</option>
-<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
-<option value="Thailand">Thailand</option>
-<option value="Timor-Leste">Timor-Leste</option>
-<option value="Togo">Togo</option>
-<option value="Tokelau">Tokelau</option>
-<option value="Tonga">Tonga</option>
-<option value="Trinidad and Tobago">Trinidad and Tobago</option>
-<option value="Tunisia">Tunisia</option>
-<option value="Turkey">Turkey</option>
-<option value="Turkmenistan">Turkmenistan</option>
-<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
-<option value="Tuvalu">Tuvalu</option>
-<option value="Uganda">Uganda</option>
-<option value="Ukraine">Ukraine</option>
-<option value="United Arab Emirates">United Arab Emirates</option>
-<option value="United Kingdom">United Kingdom</option>
-<option value="United States">United States</option>
-<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
-<option value="Uruguay">Uruguay</option>
-<option value="Uzbekistan">Uzbekistan</option>
-<option value="Vanuatu">Vanuatu</option>
-<option value="Venezuela">Venezuela</option>
-<option value="Viet Nam">Viet Nam</option>
-<option value="Virgin Islands, British">Virgin Islands, British</option>
-<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
-<option value="Wallis and Futuna">Wallis and Futuna</option>
-<option value="Western Sahara">Western Sahara</option>
-<option value="Yemen">Yemen</option>
-<option value="Zambia">Zambia</option>
-<option value="Zimbabwe">Zimbabwe</option></select>
- COUNTRIES
- assert_dom_equal(expected_select[0..-2], country_select("post", "origin", ["New Zealand", "Nicaragua"]))
- end
-
- def test_country_select_under_fields_for
- @post = Post.new
- @post.origin = "Australia"
- expected_select = <<-COUNTRIES
-<select id="post_origin" name="post[origin]"><option value="Afghanistan">Afghanistan</option>
-<option value="Aland Islands">Aland Islands</option>
-<option value="Albania">Albania</option>
-<option value="Algeria">Algeria</option>
-<option value="American Samoa">American Samoa</option>
-<option value="Andorra">Andorra</option>
-<option value="Angola">Angola</option>
-<option value="Anguilla">Anguilla</option>
-<option value="Antarctica">Antarctica</option>
-<option value="Antigua And Barbuda">Antigua And Barbuda</option>
-<option value="Argentina">Argentina</option>
-<option value="Armenia">Armenia</option>
-<option value="Aruba">Aruba</option>
-<option selected="selected" value="Australia">Australia</option>
-<option value="Austria">Austria</option>
-<option value="Azerbaijan">Azerbaijan</option>
-<option value="Bahamas">Bahamas</option>
-<option value="Bahrain">Bahrain</option>
-<option value="Bangladesh">Bangladesh</option>
-<option value="Barbados">Barbados</option>
-<option value="Belarus">Belarus</option>
-<option value="Belgium">Belgium</option>
-<option value="Belize">Belize</option>
-<option value="Benin">Benin</option>
-<option value="Bermuda">Bermuda</option>
-<option value="Bhutan">Bhutan</option>
-<option value="Bolivia">Bolivia</option>
-<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
-<option value="Botswana">Botswana</option>
-<option value="Bouvet Island">Bouvet Island</option>
-<option value="Brazil">Brazil</option>
-<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
-<option value="Brunei Darussalam">Brunei Darussalam</option>
-<option value="Bulgaria">Bulgaria</option>
-<option value="Burkina Faso">Burkina Faso</option>
-<option value="Burundi">Burundi</option>
-<option value="Cambodia">Cambodia</option>
-<option value="Cameroon">Cameroon</option>
-<option value="Canada">Canada</option>
-<option value="Cape Verde">Cape Verde</option>
-<option value="Cayman Islands">Cayman Islands</option>
-<option value="Central African Republic">Central African Republic</option>
-<option value="Chad">Chad</option>
-<option value="Chile">Chile</option>
-<option value="China">China</option>
-<option value="Christmas Island">Christmas Island</option>
-<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
-<option value="Colombia">Colombia</option>
-<option value="Comoros">Comoros</option>
-<option value="Congo">Congo</option>
-<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
-<option value="Cook Islands">Cook Islands</option>
-<option value="Costa Rica">Costa Rica</option>
-<option value="Cote d'Ivoire">Cote d'Ivoire</option>
-<option value="Croatia">Croatia</option>
-<option value="Cuba">Cuba</option>
-<option value="Cyprus">Cyprus</option>
-<option value="Czech Republic">Czech Republic</option>
-<option value="Denmark">Denmark</option>
-<option value="Djibouti">Djibouti</option>
-<option value="Dominica">Dominica</option>
-<option value="Dominican Republic">Dominican Republic</option>
-<option value="Ecuador">Ecuador</option>
-<option value="Egypt">Egypt</option>
-<option value="El Salvador">El Salvador</option>
-<option value="Equatorial Guinea">Equatorial Guinea</option>
-<option value="Eritrea">Eritrea</option>
-<option value="Estonia">Estonia</option>
-<option value="Ethiopia">Ethiopia</option>
-<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
-<option value="Faroe Islands">Faroe Islands</option>
-<option value="Fiji">Fiji</option>
-<option value="Finland">Finland</option>
-<option value="France">France</option>
-<option value="French Guiana">French Guiana</option>
-<option value="French Polynesia">French Polynesia</option>
-<option value="French Southern Territories">French Southern Territories</option>
-<option value="Gabon">Gabon</option>
-<option value="Gambia">Gambia</option>
-<option value="Georgia">Georgia</option>
-<option value="Germany">Germany</option>
-<option value="Ghana">Ghana</option>
-<option value="Gibraltar">Gibraltar</option>
-<option value="Greece">Greece</option>
-<option value="Greenland">Greenland</option>
-<option value="Grenada">Grenada</option>
-<option value="Guadeloupe">Guadeloupe</option>
-<option value="Guam">Guam</option>
-<option value="Guatemala">Guatemala</option>
-<option value="Guernsey">Guernsey</option>
-<option value="Guinea">Guinea</option>
-<option value="Guinea-Bissau">Guinea-Bissau</option>
-<option value="Guyana">Guyana</option>
-<option value="Haiti">Haiti</option>
-<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
-<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
-<option value="Honduras">Honduras</option>
-<option value="Hong Kong">Hong Kong</option>
-<option value="Hungary">Hungary</option>
-<option value="Iceland">Iceland</option>
-<option value="India">India</option>
-<option value="Indonesia">Indonesia</option>
-<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
-<option value="Iraq">Iraq</option>
-<option value="Ireland">Ireland</option>
-<option value="Isle of Man">Isle of Man</option>
-<option value="Israel">Israel</option>
-<option value="Italy">Italy</option>
-<option value="Jamaica">Jamaica</option>
-<option value="Japan">Japan</option>
-<option value="Jersey">Jersey</option>
-<option value="Jordan">Jordan</option>
-<option value="Kazakhstan">Kazakhstan</option>
-<option value="Kenya">Kenya</option>
-<option value="Kiribati">Kiribati</option>
-<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
-<option value="Korea, Republic of">Korea, Republic of</option>
-<option value="Kuwait">Kuwait</option>
-<option value="Kyrgyzstan">Kyrgyzstan</option>
-<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
-<option value="Latvia">Latvia</option>
-<option value="Lebanon">Lebanon</option>
-<option value="Lesotho">Lesotho</option>
-<option value="Liberia">Liberia</option>
-<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
-<option value="Liechtenstein">Liechtenstein</option>
-<option value="Lithuania">Lithuania</option>
-<option value="Luxembourg">Luxembourg</option>
-<option value="Macao">Macao</option>
-<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
-<option value="Madagascar">Madagascar</option>
-<option value="Malawi">Malawi</option>
-<option value="Malaysia">Malaysia</option>
-<option value="Maldives">Maldives</option>
-<option value="Mali">Mali</option>
-<option value="Malta">Malta</option>
-<option value="Marshall Islands">Marshall Islands</option>
-<option value="Martinique">Martinique</option>
-<option value="Mauritania">Mauritania</option>
-<option value="Mauritius">Mauritius</option>
-<option value="Mayotte">Mayotte</option>
-<option value="Mexico">Mexico</option>
-<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
-<option value="Moldova, Republic of">Moldova, Republic of</option>
-<option value="Monaco">Monaco</option>
-<option value="Mongolia">Mongolia</option>
-<option value="Montenegro">Montenegro</option>
-<option value="Montserrat">Montserrat</option>
-<option value="Morocco">Morocco</option>
-<option value="Mozambique">Mozambique</option>
-<option value="Myanmar">Myanmar</option>
-<option value="Namibia">Namibia</option>
-<option value="Nauru">Nauru</option>
-<option value="Nepal">Nepal</option>
-<option value="Netherlands">Netherlands</option>
-<option value="Netherlands Antilles">Netherlands Antilles</option>
-<option value="New Caledonia">New Caledonia</option>
-<option value="New Zealand">New Zealand</option>
-<option value="Nicaragua">Nicaragua</option>
-<option value="Niger">Niger</option>
-<option value="Nigeria">Nigeria</option>
-<option value="Niue">Niue</option>
-<option value="Norfolk Island">Norfolk Island</option>
-<option value="Northern Mariana Islands">Northern Mariana Islands</option>
-<option value="Norway">Norway</option>
-<option value="Oman">Oman</option>
-<option value="Pakistan">Pakistan</option>
-<option value="Palau">Palau</option>
-<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
-<option value="Panama">Panama</option>
-<option value="Papua New Guinea">Papua New Guinea</option>
-<option value="Paraguay">Paraguay</option>
-<option value="Peru">Peru</option>
-<option value="Philippines">Philippines</option>
-<option value="Pitcairn">Pitcairn</option>
-<option value="Poland">Poland</option>
-<option value="Portugal">Portugal</option>
-<option value="Puerto Rico">Puerto Rico</option>
-<option value="Qatar">Qatar</option>
-<option value="Reunion">Reunion</option>
-<option value="Romania">Romania</option>
-<option value="Russian Federation">Russian Federation</option>
-<option value="Rwanda">Rwanda</option>
-<option value="Saint Barthelemy">Saint Barthelemy</option>
-<option value="Saint Helena">Saint Helena</option>
-<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
-<option value="Saint Lucia">Saint Lucia</option>
-<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
-<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
-<option value="Samoa">Samoa</option>
-<option value="San Marino">San Marino</option>
-<option value="Sao Tome and Principe">Sao Tome and Principe</option>
-<option value="Saudi Arabia">Saudi Arabia</option>
-<option value="Senegal">Senegal</option>
-<option value="Serbia">Serbia</option>
-<option value="Seychelles">Seychelles</option>
-<option value="Sierra Leone">Sierra Leone</option>
-<option value="Singapore">Singapore</option>
-<option value="Slovakia">Slovakia</option>
-<option value="Slovenia">Slovenia</option>
-<option value="Solomon Islands">Solomon Islands</option>
-<option value="Somalia">Somalia</option>
-<option value="South Africa">South Africa</option>
-<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
-<option value="Spain">Spain</option>
-<option value="Sri Lanka">Sri Lanka</option>
-<option value="Sudan">Sudan</option>
-<option value="Suriname">Suriname</option>
-<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
-<option value="Swaziland">Swaziland</option>
-<option value="Sweden">Sweden</option>
-<option value="Switzerland">Switzerland</option>
-<option value="Syrian Arab Republic">Syrian Arab Republic</option>
-<option value="Taiwan, Province of China">Taiwan, Province of China</option>
-<option value="Tajikistan">Tajikistan</option>
-<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
-<option value="Thailand">Thailand</option>
-<option value="Timor-Leste">Timor-Leste</option>
-<option value="Togo">Togo</option>
-<option value="Tokelau">Tokelau</option>
-<option value="Tonga">Tonga</option>
-<option value="Trinidad and Tobago">Trinidad and Tobago</option>
-<option value="Tunisia">Tunisia</option>
-<option value="Turkey">Turkey</option>
-<option value="Turkmenistan">Turkmenistan</option>
-<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
-<option value="Tuvalu">Tuvalu</option>
-<option value="Uganda">Uganda</option>
-<option value="Ukraine">Ukraine</option>
-<option value="United Arab Emirates">United Arab Emirates</option>
-<option value="United Kingdom">United Kingdom</option>
-<option value="United States">United States</option>
-<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
-<option value="Uruguay">Uruguay</option>
-<option value="Uzbekistan">Uzbekistan</option>
-<option value="Vanuatu">Vanuatu</option>
-<option value="Venezuela">Venezuela</option>
-<option value="Viet Nam">Viet Nam</option>
-<option value="Virgin Islands, British">Virgin Islands, British</option>
-<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
-<option value="Wallis and Futuna">Wallis and Futuna</option>
-<option value="Western Sahara">Western Sahara</option>
-<option value="Yemen">Yemen</option>
-<option value="Zambia">Zambia</option>
-<option value="Zimbabwe">Zimbabwe</option></select>
- COUNTRIES
-
- fields_for :post, @post do |f|
- concat f.country_select("origin")
- end
-
- assert_dom_equal(expected_select[0..-2], output_buffer)
- end
-
- def test_country_select_under_fields_for_with_index
- @post = Post.new
- @post.origin = "United States"
- expected_select = <<-COUNTRIES
-<select id="post_325_origin" name="post[325][origin]"><option value="Afghanistan">Afghanistan</option>
-<option value="Aland Islands">Aland Islands</option>
-<option value="Albania">Albania</option>
-<option value="Algeria">Algeria</option>
-<option value="American Samoa">American Samoa</option>
-<option value="Andorra">Andorra</option>
-<option value="Angola">Angola</option>
-<option value="Anguilla">Anguilla</option>
-<option value="Antarctica">Antarctica</option>
-<option value="Antigua And Barbuda">Antigua And Barbuda</option>
-<option value="Argentina">Argentina</option>
-<option value="Armenia">Armenia</option>
-<option value="Aruba">Aruba</option>
-<option value="Australia">Australia</option>
-<option value="Austria">Austria</option>
-<option value="Azerbaijan">Azerbaijan</option>
-<option value="Bahamas">Bahamas</option>
-<option value="Bahrain">Bahrain</option>
-<option value="Bangladesh">Bangladesh</option>
-<option value="Barbados">Barbados</option>
-<option value="Belarus">Belarus</option>
-<option value="Belgium">Belgium</option>
-<option value="Belize">Belize</option>
-<option value="Benin">Benin</option>
-<option value="Bermuda">Bermuda</option>
-<option value="Bhutan">Bhutan</option>
-<option value="Bolivia">Bolivia</option>
-<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
-<option value="Botswana">Botswana</option>
-<option value="Bouvet Island">Bouvet Island</option>
-<option value="Brazil">Brazil</option>
-<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
-<option value="Brunei Darussalam">Brunei Darussalam</option>
-<option value="Bulgaria">Bulgaria</option>
-<option value="Burkina Faso">Burkina Faso</option>
-<option value="Burundi">Burundi</option>
-<option value="Cambodia">Cambodia</option>
-<option value="Cameroon">Cameroon</option>
-<option value="Canada">Canada</option>
-<option value="Cape Verde">Cape Verde</option>
-<option value="Cayman Islands">Cayman Islands</option>
-<option value="Central African Republic">Central African Republic</option>
-<option value="Chad">Chad</option>
-<option value="Chile">Chile</option>
-<option value="China">China</option>
-<option value="Christmas Island">Christmas Island</option>
-<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
-<option value="Colombia">Colombia</option>
-<option value="Comoros">Comoros</option>
-<option value="Congo">Congo</option>
-<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
-<option value="Cook Islands">Cook Islands</option>
-<option value="Costa Rica">Costa Rica</option>
-<option value="Cote d'Ivoire">Cote d'Ivoire</option>
-<option value="Croatia">Croatia</option>
-<option value="Cuba">Cuba</option>
-<option value="Cyprus">Cyprus</option>
-<option value="Czech Republic">Czech Republic</option>
-<option value="Denmark">Denmark</option>
-<option value="Djibouti">Djibouti</option>
-<option value="Dominica">Dominica</option>
-<option value="Dominican Republic">Dominican Republic</option>
-<option value="Ecuador">Ecuador</option>
-<option value="Egypt">Egypt</option>
-<option value="El Salvador">El Salvador</option>
-<option value="Equatorial Guinea">Equatorial Guinea</option>
-<option value="Eritrea">Eritrea</option>
-<option value="Estonia">Estonia</option>
-<option value="Ethiopia">Ethiopia</option>
-<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
-<option value="Faroe Islands">Faroe Islands</option>
-<option value="Fiji">Fiji</option>
-<option value="Finland">Finland</option>
-<option value="France">France</option>
-<option value="French Guiana">French Guiana</option>
-<option value="French Polynesia">French Polynesia</option>
-<option value="French Southern Territories">French Southern Territories</option>
-<option value="Gabon">Gabon</option>
-<option value="Gambia">Gambia</option>
-<option value="Georgia">Georgia</option>
-<option value="Germany">Germany</option>
-<option value="Ghana">Ghana</option>
-<option value="Gibraltar">Gibraltar</option>
-<option value="Greece">Greece</option>
-<option value="Greenland">Greenland</option>
-<option value="Grenada">Grenada</option>
-<option value="Guadeloupe">Guadeloupe</option>
-<option value="Guam">Guam</option>
-<option value="Guatemala">Guatemala</option>
-<option value="Guernsey">Guernsey</option>
-<option value="Guinea">Guinea</option>
-<option value="Guinea-Bissau">Guinea-Bissau</option>
-<option value="Guyana">Guyana</option>
-<option value="Haiti">Haiti</option>
-<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
-<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
-<option value="Honduras">Honduras</option>
-<option value="Hong Kong">Hong Kong</option>
-<option value="Hungary">Hungary</option>
-<option value="Iceland">Iceland</option>
-<option value="India">India</option>
-<option value="Indonesia">Indonesia</option>
-<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
-<option value="Iraq">Iraq</option>
-<option value="Ireland">Ireland</option>
-<option value="Isle of Man">Isle of Man</option>
-<option value="Israel">Israel</option>
-<option value="Italy">Italy</option>
-<option value="Jamaica">Jamaica</option>
-<option value="Japan">Japan</option>
-<option value="Jersey">Jersey</option>
-<option value="Jordan">Jordan</option>
-<option value="Kazakhstan">Kazakhstan</option>
-<option value="Kenya">Kenya</option>
-<option value="Kiribati">Kiribati</option>
-<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
-<option value="Korea, Republic of">Korea, Republic of</option>
-<option value="Kuwait">Kuwait</option>
-<option value="Kyrgyzstan">Kyrgyzstan</option>
-<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
-<option value="Latvia">Latvia</option>
-<option value="Lebanon">Lebanon</option>
-<option value="Lesotho">Lesotho</option>
-<option value="Liberia">Liberia</option>
-<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
-<option value="Liechtenstein">Liechtenstein</option>
-<option value="Lithuania">Lithuania</option>
-<option value="Luxembourg">Luxembourg</option>
-<option value="Macao">Macao</option>
-<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
-<option value="Madagascar">Madagascar</option>
-<option value="Malawi">Malawi</option>
-<option value="Malaysia">Malaysia</option>
-<option value="Maldives">Maldives</option>
-<option value="Mali">Mali</option>
-<option value="Malta">Malta</option>
-<option value="Marshall Islands">Marshall Islands</option>
-<option value="Martinique">Martinique</option>
-<option value="Mauritania">Mauritania</option>
-<option value="Mauritius">Mauritius</option>
-<option value="Mayotte">Mayotte</option>
-<option value="Mexico">Mexico</option>
-<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
-<option value="Moldova, Republic of">Moldova, Republic of</option>
-<option value="Monaco">Monaco</option>
-<option value="Mongolia">Mongolia</option>
-<option value="Montenegro">Montenegro</option>
-<option value="Montserrat">Montserrat</option>
-<option value="Morocco">Morocco</option>
-<option value="Mozambique">Mozambique</option>
-<option value="Myanmar">Myanmar</option>
-<option value="Namibia">Namibia</option>
-<option value="Nauru">Nauru</option>
-<option value="Nepal">Nepal</option>
-<option value="Netherlands">Netherlands</option>
-<option value="Netherlands Antilles">Netherlands Antilles</option>
-<option value="New Caledonia">New Caledonia</option>
-<option value="New Zealand">New Zealand</option>
-<option value="Nicaragua">Nicaragua</option>
-<option value="Niger">Niger</option>
-<option value="Nigeria">Nigeria</option>
-<option value="Niue">Niue</option>
-<option value="Norfolk Island">Norfolk Island</option>
-<option value="Northern Mariana Islands">Northern Mariana Islands</option>
-<option value="Norway">Norway</option>
-<option value="Oman">Oman</option>
-<option value="Pakistan">Pakistan</option>
-<option value="Palau">Palau</option>
-<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
-<option value="Panama">Panama</option>
-<option value="Papua New Guinea">Papua New Guinea</option>
-<option value="Paraguay">Paraguay</option>
-<option value="Peru">Peru</option>
-<option value="Philippines">Philippines</option>
-<option value="Pitcairn">Pitcairn</option>
-<option value="Poland">Poland</option>
-<option value="Portugal">Portugal</option>
-<option value="Puerto Rico">Puerto Rico</option>
-<option value="Qatar">Qatar</option>
-<option value="Reunion">Reunion</option>
-<option value="Romania">Romania</option>
-<option value="Russian Federation">Russian Federation</option>
-<option value="Rwanda">Rwanda</option>
-<option value="Saint Barthelemy">Saint Barthelemy</option>
-<option value="Saint Helena">Saint Helena</option>
-<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
-<option value="Saint Lucia">Saint Lucia</option>
-<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
-<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
-<option value="Samoa">Samoa</option>
-<option value="San Marino">San Marino</option>
-<option value="Sao Tome and Principe">Sao Tome and Principe</option>
-<option value="Saudi Arabia">Saudi Arabia</option>
-<option value="Senegal">Senegal</option>
-<option value="Serbia">Serbia</option>
-<option value="Seychelles">Seychelles</option>
-<option value="Sierra Leone">Sierra Leone</option>
-<option value="Singapore">Singapore</option>
-<option value="Slovakia">Slovakia</option>
-<option value="Slovenia">Slovenia</option>
-<option value="Solomon Islands">Solomon Islands</option>
-<option value="Somalia">Somalia</option>
-<option value="South Africa">South Africa</option>
-<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
-<option value="Spain">Spain</option>
-<option value="Sri Lanka">Sri Lanka</option>
-<option value="Sudan">Sudan</option>
-<option value="Suriname">Suriname</option>
-<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
-<option value="Swaziland">Swaziland</option>
-<option value="Sweden">Sweden</option>
-<option value="Switzerland">Switzerland</option>
-<option value="Syrian Arab Republic">Syrian Arab Republic</option>
-<option value="Taiwan, Province of China">Taiwan, Province of China</option>
-<option value="Tajikistan">Tajikistan</option>
-<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
-<option value="Thailand">Thailand</option>
-<option value="Timor-Leste">Timor-Leste</option>
-<option value="Togo">Togo</option>
-<option value="Tokelau">Tokelau</option>
-<option value="Tonga">Tonga</option>
-<option value="Trinidad and Tobago">Trinidad and Tobago</option>
-<option value="Tunisia">Tunisia</option>
-<option value="Turkey">Turkey</option>
-<option value="Turkmenistan">Turkmenistan</option>
-<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
-<option value="Tuvalu">Tuvalu</option>
-<option value="Uganda">Uganda</option>
-<option value="Ukraine">Ukraine</option>
-<option value="United Arab Emirates">United Arab Emirates</option>
-<option value="United Kingdom">United Kingdom</option>
-<option selected="selected" value="United States">United States</option>
-<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
-<option value="Uruguay">Uruguay</option>
-<option value="Uzbekistan">Uzbekistan</option>
-<option value="Vanuatu">Vanuatu</option>
-<option value="Venezuela">Venezuela</option>
-<option value="Viet Nam">Viet Nam</option>
-<option value="Virgin Islands, British">Virgin Islands, British</option>
-<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
-<option value="Wallis and Futuna">Wallis and Futuna</option>
-<option value="Western Sahara">Western Sahara</option>
-<option value="Yemen">Yemen</option>
-<option value="Zambia">Zambia</option>
-<option value="Zimbabwe">Zimbabwe</option></select>
- COUNTRIES
-
- fields_for :post, @post, :index => 325 do |f|
- concat f.country_select("origin")
- end
-
- assert_dom_equal(expected_select[0..-2], output_buffer)
- end
-
- def test_country_select_under_fields_for_with_auto_index
- @post = Post.new
- @post.origin = "Iraq"
- def @post.to_param; 325; end
-
- expected_select = <<-COUNTRIES
-<select id="post_325_origin" name="post[325][origin]"><option value="Afghanistan">Afghanistan</option>
-<option value="Aland Islands">Aland Islands</option>
-<option value="Albania">Albania</option>
-<option value="Algeria">Algeria</option>
-<option value="American Samoa">American Samoa</option>
-<option value="Andorra">Andorra</option>
-<option value="Angola">Angola</option>
-<option value="Anguilla">Anguilla</option>
-<option value="Antarctica">Antarctica</option>
-<option value="Antigua And Barbuda">Antigua And Barbuda</option>
-<option value="Argentina">Argentina</option>
-<option value="Armenia">Armenia</option>
-<option value="Aruba">Aruba</option>
-<option value="Australia">Australia</option>
-<option value="Austria">Austria</option>
-<option value="Azerbaijan">Azerbaijan</option>
-<option value="Bahamas">Bahamas</option>
-<option value="Bahrain">Bahrain</option>
-<option value="Bangladesh">Bangladesh</option>
-<option value="Barbados">Barbados</option>
-<option value="Belarus">Belarus</option>
-<option value="Belgium">Belgium</option>
-<option value="Belize">Belize</option>
-<option value="Benin">Benin</option>
-<option value="Bermuda">Bermuda</option>
-<option value="Bhutan">Bhutan</option>
-<option value="Bolivia">Bolivia</option>
-<option value="Bosnia and Herzegowina">Bosnia and Herzegowina</option>
-<option value="Botswana">Botswana</option>
-<option value="Bouvet Island">Bouvet Island</option>
-<option value="Brazil">Brazil</option>
-<option value="British Indian Ocean Territory">British Indian Ocean Territory</option>
-<option value="Brunei Darussalam">Brunei Darussalam</option>
-<option value="Bulgaria">Bulgaria</option>
-<option value="Burkina Faso">Burkina Faso</option>
-<option value="Burundi">Burundi</option>
-<option value="Cambodia">Cambodia</option>
-<option value="Cameroon">Cameroon</option>
-<option value="Canada">Canada</option>
-<option value="Cape Verde">Cape Verde</option>
-<option value="Cayman Islands">Cayman Islands</option>
-<option value="Central African Republic">Central African Republic</option>
-<option value="Chad">Chad</option>
-<option value="Chile">Chile</option>
-<option value="China">China</option>
-<option value="Christmas Island">Christmas Island</option>
-<option value="Cocos (Keeling) Islands">Cocos (Keeling) Islands</option>
-<option value="Colombia">Colombia</option>
-<option value="Comoros">Comoros</option>
-<option value="Congo">Congo</option>
-<option value="Congo, the Democratic Republic of the">Congo, the Democratic Republic of the</option>
-<option value="Cook Islands">Cook Islands</option>
-<option value="Costa Rica">Costa Rica</option>
-<option value="Cote d'Ivoire">Cote d'Ivoire</option>
-<option value="Croatia">Croatia</option>
-<option value="Cuba">Cuba</option>
-<option value="Cyprus">Cyprus</option>
-<option value="Czech Republic">Czech Republic</option>
-<option value="Denmark">Denmark</option>
-<option value="Djibouti">Djibouti</option>
-<option value="Dominica">Dominica</option>
-<option value="Dominican Republic">Dominican Republic</option>
-<option value="Ecuador">Ecuador</option>
-<option value="Egypt">Egypt</option>
-<option value="El Salvador">El Salvador</option>
-<option value="Equatorial Guinea">Equatorial Guinea</option>
-<option value="Eritrea">Eritrea</option>
-<option value="Estonia">Estonia</option>
-<option value="Ethiopia">Ethiopia</option>
-<option value="Falkland Islands (Malvinas)">Falkland Islands (Malvinas)</option>
-<option value="Faroe Islands">Faroe Islands</option>
-<option value="Fiji">Fiji</option>
-<option value="Finland">Finland</option>
-<option value="France">France</option>
-<option value="French Guiana">French Guiana</option>
-<option value="French Polynesia">French Polynesia</option>
-<option value="French Southern Territories">French Southern Territories</option>
-<option value="Gabon">Gabon</option>
-<option value="Gambia">Gambia</option>
-<option value="Georgia">Georgia</option>
-<option value="Germany">Germany</option>
-<option value="Ghana">Ghana</option>
-<option value="Gibraltar">Gibraltar</option>
-<option value="Greece">Greece</option>
-<option value="Greenland">Greenland</option>
-<option value="Grenada">Grenada</option>
-<option value="Guadeloupe">Guadeloupe</option>
-<option value="Guam">Guam</option>
-<option value="Guatemala">Guatemala</option>
-<option value="Guernsey">Guernsey</option>
-<option value="Guinea">Guinea</option>
-<option value="Guinea-Bissau">Guinea-Bissau</option>
-<option value="Guyana">Guyana</option>
-<option value="Haiti">Haiti</option>
-<option value="Heard and McDonald Islands">Heard and McDonald Islands</option>
-<option value="Holy See (Vatican City State)">Holy See (Vatican City State)</option>
-<option value="Honduras">Honduras</option>
-<option value="Hong Kong">Hong Kong</option>
-<option value="Hungary">Hungary</option>
-<option value="Iceland">Iceland</option>
-<option value="India">India</option>
-<option value="Indonesia">Indonesia</option>
-<option value="Iran, Islamic Republic of">Iran, Islamic Republic of</option>
-<option selected="selected" value="Iraq">Iraq</option>
-<option value="Ireland">Ireland</option>
-<option value="Isle of Man">Isle of Man</option>
-<option value="Israel">Israel</option>
-<option value="Italy">Italy</option>
-<option value="Jamaica">Jamaica</option>
-<option value="Japan">Japan</option>
-<option value="Jersey">Jersey</option>
-<option value="Jordan">Jordan</option>
-<option value="Kazakhstan">Kazakhstan</option>
-<option value="Kenya">Kenya</option>
-<option value="Kiribati">Kiribati</option>
-<option value="Korea, Democratic People's Republic of">Korea, Democratic People's Republic of</option>
-<option value="Korea, Republic of">Korea, Republic of</option>
-<option value="Kuwait">Kuwait</option>
-<option value="Kyrgyzstan">Kyrgyzstan</option>
-<option value="Lao People's Democratic Republic">Lao People's Democratic Republic</option>
-<option value="Latvia">Latvia</option>
-<option value="Lebanon">Lebanon</option>
-<option value="Lesotho">Lesotho</option>
-<option value="Liberia">Liberia</option>
-<option value="Libyan Arab Jamahiriya">Libyan Arab Jamahiriya</option>
-<option value="Liechtenstein">Liechtenstein</option>
-<option value="Lithuania">Lithuania</option>
-<option value="Luxembourg">Luxembourg</option>
-<option value="Macao">Macao</option>
-<option value="Macedonia, The Former Yugoslav Republic Of">Macedonia, The Former Yugoslav Republic Of</option>
-<option value="Madagascar">Madagascar</option>
-<option value="Malawi">Malawi</option>
-<option value="Malaysia">Malaysia</option>
-<option value="Maldives">Maldives</option>
-<option value="Mali">Mali</option>
-<option value="Malta">Malta</option>
-<option value="Marshall Islands">Marshall Islands</option>
-<option value="Martinique">Martinique</option>
-<option value="Mauritania">Mauritania</option>
-<option value="Mauritius">Mauritius</option>
-<option value="Mayotte">Mayotte</option>
-<option value="Mexico">Mexico</option>
-<option value="Micronesia, Federated States of">Micronesia, Federated States of</option>
-<option value="Moldova, Republic of">Moldova, Republic of</option>
-<option value="Monaco">Monaco</option>
-<option value="Mongolia">Mongolia</option>
-<option value="Montenegro">Montenegro</option>
-<option value="Montserrat">Montserrat</option>
-<option value="Morocco">Morocco</option>
-<option value="Mozambique">Mozambique</option>
-<option value="Myanmar">Myanmar</option>
-<option value="Namibia">Namibia</option>
-<option value="Nauru">Nauru</option>
-<option value="Nepal">Nepal</option>
-<option value="Netherlands">Netherlands</option>
-<option value="Netherlands Antilles">Netherlands Antilles</option>
-<option value="New Caledonia">New Caledonia</option>
-<option value="New Zealand">New Zealand</option>
-<option value="Nicaragua">Nicaragua</option>
-<option value="Niger">Niger</option>
-<option value="Nigeria">Nigeria</option>
-<option value="Niue">Niue</option>
-<option value="Norfolk Island">Norfolk Island</option>
-<option value="Northern Mariana Islands">Northern Mariana Islands</option>
-<option value="Norway">Norway</option>
-<option value="Oman">Oman</option>
-<option value="Pakistan">Pakistan</option>
-<option value="Palau">Palau</option>
-<option value="Palestinian Territory, Occupied">Palestinian Territory, Occupied</option>
-<option value="Panama">Panama</option>
-<option value="Papua New Guinea">Papua New Guinea</option>
-<option value="Paraguay">Paraguay</option>
-<option value="Peru">Peru</option>
-<option value="Philippines">Philippines</option>
-<option value="Pitcairn">Pitcairn</option>
-<option value="Poland">Poland</option>
-<option value="Portugal">Portugal</option>
-<option value="Puerto Rico">Puerto Rico</option>
-<option value="Qatar">Qatar</option>
-<option value="Reunion">Reunion</option>
-<option value="Romania">Romania</option>
-<option value="Russian Federation">Russian Federation</option>
-<option value="Rwanda">Rwanda</option>
-<option value="Saint Barthelemy">Saint Barthelemy</option>
-<option value="Saint Helena">Saint Helena</option>
-<option value="Saint Kitts and Nevis">Saint Kitts and Nevis</option>
-<option value="Saint Lucia">Saint Lucia</option>
-<option value="Saint Pierre and Miquelon">Saint Pierre and Miquelon</option>
-<option value="Saint Vincent and the Grenadines">Saint Vincent and the Grenadines</option>
-<option value="Samoa">Samoa</option>
-<option value="San Marino">San Marino</option>
-<option value="Sao Tome and Principe">Sao Tome and Principe</option>
-<option value="Saudi Arabia">Saudi Arabia</option>
-<option value="Senegal">Senegal</option>
-<option value="Serbia">Serbia</option>
-<option value="Seychelles">Seychelles</option>
-<option value="Sierra Leone">Sierra Leone</option>
-<option value="Singapore">Singapore</option>
-<option value="Slovakia">Slovakia</option>
-<option value="Slovenia">Slovenia</option>
-<option value="Solomon Islands">Solomon Islands</option>
-<option value="Somalia">Somalia</option>
-<option value="South Africa">South Africa</option>
-<option value="South Georgia and the South Sandwich Islands">South Georgia and the South Sandwich Islands</option>
-<option value="Spain">Spain</option>
-<option value="Sri Lanka">Sri Lanka</option>
-<option value="Sudan">Sudan</option>
-<option value="Suriname">Suriname</option>
-<option value="Svalbard and Jan Mayen">Svalbard and Jan Mayen</option>
-<option value="Swaziland">Swaziland</option>
-<option value="Sweden">Sweden</option>
-<option value="Switzerland">Switzerland</option>
-<option value="Syrian Arab Republic">Syrian Arab Republic</option>
-<option value="Taiwan, Province of China">Taiwan, Province of China</option>
-<option value="Tajikistan">Tajikistan</option>
-<option value="Tanzania, United Republic of">Tanzania, United Republic of</option>
-<option value="Thailand">Thailand</option>
-<option value="Timor-Leste">Timor-Leste</option>
-<option value="Togo">Togo</option>
-<option value="Tokelau">Tokelau</option>
-<option value="Tonga">Tonga</option>
-<option value="Trinidad and Tobago">Trinidad and Tobago</option>
-<option value="Tunisia">Tunisia</option>
-<option value="Turkey">Turkey</option>
-<option value="Turkmenistan">Turkmenistan</option>
-<option value="Turks and Caicos Islands">Turks and Caicos Islands</option>
-<option value="Tuvalu">Tuvalu</option>
-<option value="Uganda">Uganda</option>
-<option value="Ukraine">Ukraine</option>
-<option value="United Arab Emirates">United Arab Emirates</option>
-<option value="United Kingdom">United Kingdom</option>
-<option value="United States">United States</option>
-<option value="United States Minor Outlying Islands">United States Minor Outlying Islands</option>
-<option value="Uruguay">Uruguay</option>
-<option value="Uzbekistan">Uzbekistan</option>
-<option value="Vanuatu">Vanuatu</option>
-<option value="Venezuela">Venezuela</option>
-<option value="Viet Nam">Viet Nam</option>
-<option value="Virgin Islands, British">Virgin Islands, British</option>
-<option value="Virgin Islands, U.S.">Virgin Islands, U.S.</option>
-<option value="Wallis and Futuna">Wallis and Futuna</option>
-<option value="Western Sahara">Western Sahara</option>
-<option value="Yemen">Yemen</option>
-<option value="Zambia">Zambia</option>
-<option value="Zimbabwe">Zimbabwe</option></select>
- COUNTRIES
-
- fields_for "post[]", @post do |f|
- concat f.country_select("origin")
- end
-
- assert_dom_equal(expected_select[0..-2], output_buffer)
- end
-
def test_time_zone_select
@firm = Firm.new("D")
html = time_zone_select( "firm", "time_zone" )
diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb
index 4e4102aec7..9b41ff8179 100644
--- a/actionpack/test/template/form_tag_helper_test.rb
+++ b/actionpack/test/template/form_tag_helper_test.rb
@@ -190,6 +190,12 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, actual
end
+ def test_label_tag_with_symbol
+ actual = label_tag :title
+ expected = %(<label for="title">Title</label>)
+ assert_dom_equal expected, actual
+ end
+
def test_label_tag_with_text
actual = label_tag "title", "My Title"
expected = %(<label for="title">My Title</label>)
diff --git a/actionpack/test/template/javascript_helper_test.rb b/actionpack/test/template/javascript_helper_test.rb
index b2c4a130c8..d41111127b 100644
--- a/actionpack/test/template/javascript_helper_test.rb
+++ b/actionpack/test/template/javascript_helper_test.rb
@@ -3,6 +3,12 @@ require 'abstract_unit'
class JavaScriptHelperTest < ActionView::TestCase
tests ActionView::Helpers::JavaScriptHelper
+ attr_accessor :template_format, :output_buffer
+
+ def setup
+ @template = self
+ end
+
def test_escape_javascript
assert_equal '', escape_javascript(nil)
assert_equal %(This \\"thing\\" is really\\n netos\\'), escape_javascript(%(This "thing" is really\n netos'))
diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb
new file mode 100644
index 0000000000..2ee7f43a65
--- /dev/null
+++ b/actionpack/test/template/number_helper_i18n_test.rb
@@ -0,0 +1,54 @@
+require 'abstract_unit'
+
+class NumberHelperI18nTests < Test::Unit::TestCase
+ include ActionView::Helpers::NumberHelper
+
+ attr_reader :request
+
+ uses_mocha 'number_helper_i18n_tests' do
+ def setup
+ @number_defaults = { :precision => 3, :delimiter => ',', :separator => '.' }
+ @currency_defaults = { :unit => '$', :format => '%u%n', :precision => 2 }
+ @human_defaults = { :precision => 1 }
+ @percentage_defaults = { :delimiter => '' }
+ @precision_defaults = { :delimiter => '' }
+
+ I18n.backend.store_translations 'en-US', :number => { :format => @number_defaults,
+ :currency => { :format => @currency_defaults }, :human => @human_defaults }
+ end
+
+ def test_number_to_currency_translates_currency_formats
+ I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults)
+ I18n.expects(:translate).with(:'number.currency.format', :locale => 'en-US',
+ :raise => true).returns(@currency_defaults)
+ number_to_currency(1, :locale => 'en-US')
+ end
+
+ def test_number_with_precision_translates_number_formats
+ I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults)
+ I18n.expects(:translate).with(:'number.precision.format', :locale => 'en-US',
+ :raise => true).returns(@precision_defaults)
+ number_with_precision(1, :locale => 'en-US')
+ end
+
+ def test_number_with_delimiter_translates_number_formats
+ I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults)
+ number_with_delimiter(1, :locale => 'en-US')
+ end
+
+ def test_number_to_percentage_translates_number_formats
+ I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults)
+ I18n.expects(:translate).with(:'number.percentage.format', :locale => 'en-US',
+ :raise => true).returns(@percentage_defaults)
+ number_to_percentage(1, :locale => 'en-US')
+ end
+
+ def test_number_to_human_size_translates_human_formats
+ I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults)
+ I18n.expects(:translate).with(:'number.human.format', :locale => 'en-US',
+ :raise => true).returns(@human_defaults)
+ # can't be called with 1 because this directly returns without calling I18n.translate
+ number_to_human_size(1025, :locale => 'en-US')
+ end
+ end
+end
diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb
index 4a8d09b544..9c9f54936c 100644
--- a/actionpack/test/template/number_helper_test.rb
+++ b/actionpack/test/template/number_helper_test.rb
@@ -26,7 +26,8 @@ class NumberHelperTest < ActionView::TestCase
assert_equal("&pound;1234567890,50", number_to_currency(1234567890.50, {:unit => "&pound;", :separator => ",", :delimiter => ""}))
assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50"))
assert_equal("1,234,567,890.50 K&#269;", number_to_currency("1234567890.50", {:unit => "K&#269;", :format => "%n %u"}))
- assert_equal("$x.", number_to_currency("x"))
+ #assert_equal("$x.", number_to_currency("x")) # fails due to API consolidation
+ assert_equal("$x", number_to_currency("x"))
assert_nil number_to_currency(nil)
end
@@ -35,7 +36,9 @@ class NumberHelperTest < ActionView::TestCase
assert_equal("100%", number_to_percentage(100, {:precision => 0}))
assert_equal("302.06%", number_to_percentage(302.0574, {:precision => 2}))
assert_equal("100.000%", number_to_percentage("100"))
+ assert_equal("1000.000%", number_to_percentage("1000"))
assert_equal("x%", number_to_percentage("x"))
+ assert_equal("1.000,000%", number_to_percentage(1000, :delimiter => '.', :separator => ','))
assert_nil number_to_percentage(nil)
end
@@ -54,21 +57,33 @@ class NumberHelperTest < ActionView::TestCase
assert_nil number_with_delimiter(nil)
end
+ def test_number_with_delimiter_with_options_hash
+ assert_equal '12 345 678', number_with_delimiter(12345678, :delimiter => ' ')
+ assert_equal '12,345,678-05', number_with_delimiter(12345678.05, :separator => '-')
+ assert_equal '12.345.678,05', number_with_delimiter(12345678.05, :separator => ',', :delimiter => '.')
+ assert_equal '12.345.678,05', number_with_delimiter(12345678.05, :delimiter => '.', :separator => ',')
+ end
+
def test_number_with_precision
assert_equal("111.235", number_with_precision(111.2346))
- assert_equal("31.83", number_with_precision(31.825, 2))
- assert_equal("111.23", number_with_precision(111.2346, 2))
- assert_equal("111.00", number_with_precision(111, 2))
+ assert_equal("31.83", number_with_precision(31.825, :precision => 2))
+ assert_equal("111.23", number_with_precision(111.2346, :precision => 2))
+ assert_equal("111.00", number_with_precision(111, :precision => 2))
assert_equal("111.235", number_with_precision("111.2346"))
- assert_equal("31.83", number_with_precision("31.825", 2))
- assert_equal("112", number_with_precision(111.50, 0))
- assert_equal("1234567892", number_with_precision(1234567891.50, 0))
+ assert_equal("31.83", number_with_precision("31.825", :precision => 2))
+ assert_equal("112", number_with_precision(111.50, :precision => 0))
+ assert_equal("1234567892", number_with_precision(1234567891.50, :precision => 0))
# Return non-numeric params unchanged.
assert_equal("x", number_with_precision("x"))
assert_nil number_with_precision(nil)
end
+ def test_number_with_precision_with_custom_delimiter_and_separator
+ assert_equal '31,83', number_with_precision(31.825, :precision => 2, :separator => ',')
+ assert_equal '1.231,83', number_with_precision(1231.825, :precision => 2, :separator => ',', :delimiter => '.')
+ end
+
def test_number_to_human_size
assert_equal '0 Bytes', number_to_human_size(0)
assert_equal '1 Byte', number_to_human_size(1)
@@ -80,18 +95,33 @@ class NumberHelperTest < ActionView::TestCase
assert_equal '1.2 MB', number_to_human_size(1234567)
assert_equal '1.1 GB', number_to_human_size(1234567890)
assert_equal '1.1 TB', number_to_human_size(1234567890123)
+ assert_equal '1025 TB', number_to_human_size(1025.terabytes)
assert_equal '444 KB', number_to_human_size(444.kilobytes)
assert_equal '1023 MB', number_to_human_size(1023.megabytes)
assert_equal '3 TB', number_to_human_size(3.terabytes)
- assert_equal '1.18 MB', number_to_human_size(1234567, 2)
- assert_equal '3 Bytes', number_to_human_size(3.14159265, 4)
+ assert_equal '1.18 MB', number_to_human_size(1234567, :precision => 2)
+ assert_equal '3 Bytes', number_to_human_size(3.14159265, :precision => 4)
assert_equal("123 Bytes", number_to_human_size("123"))
- assert_equal '1.01 KB', number_to_human_size(1.0123.kilobytes, 2)
- assert_equal '1.01 KB', number_to_human_size(1.0100.kilobytes, 4)
- assert_equal '10 KB', number_to_human_size(10.000.kilobytes, 4)
+ assert_equal '1.01 KB', number_to_human_size(1.0123.kilobytes, :precision => 2)
+ assert_equal '1.01 KB', number_to_human_size(1.0100.kilobytes, :precision => 4)
+ assert_equal '10 KB', number_to_human_size(10.000.kilobytes, :precision => 4)
assert_equal '1 Byte', number_to_human_size(1.1)
assert_equal '10 Bytes', number_to_human_size(10)
- assert_nil number_to_human_size('x')
+ #assert_nil number_to_human_size('x') # fails due to API consolidation
assert_nil number_to_human_size(nil)
end
+
+ def test_number_to_human_size_with_options_hash
+ assert_equal '1.18 MB', number_to_human_size(1234567, :precision => 2)
+ assert_equal '3 Bytes', number_to_human_size(3.14159265, :precision => 4)
+ assert_equal '1.01 KB', number_to_human_size(1.0123.kilobytes, :precision => 2)
+ assert_equal '1.01 KB', number_to_human_size(1.0100.kilobytes, :precision => 4)
+ assert_equal '10 KB', number_to_human_size(10.000.kilobytes, :precision => 4)
+ end
+
+ def test_number_to_human_size_with_custom_delimiter_and_separator
+ assert_equal '1,01 KB', number_to_human_size(1.0123.kilobytes, :precision => 2, :separator => ',')
+ assert_equal '1,01 KB', number_to_human_size(1.0100.kilobytes, :precision => 4, :separator => ',')
+ assert_equal '1.000,1 TB', number_to_human_size(1000.1.terabytes, :delimiter => '.', :separator => ',')
+ end
end
diff --git a/actionpack/test/template/prototype_helper_test.rb b/actionpack/test/template/prototype_helper_test.rb
index 1d9bc5eb9b..abc9f930dd 100644
--- a/actionpack/test/template/prototype_helper_test.rb
+++ b/actionpack/test/template/prototype_helper_test.rb
@@ -25,10 +25,10 @@ class Author::Nested < Author; end
class PrototypeHelperBaseTest < ActionView::TestCase
- attr_accessor :template_format
+ attr_accessor :template_format, :output_buffer
def setup
- @template = nil
+ @template = self
@controller = Class.new do
def url_for(options)
if options.is_a?(String)
@@ -77,6 +77,8 @@ class PrototypeHelperTest < PrototypeHelperBaseTest
link_to_remote("Remote outauthor", :failure => "alert(request.responseText)", :url => { :action => "whatnot" })
assert_dom_equal %(<a href=\"#\" onclick=\"new Ajax.Request('http://www.example.com/whatnot?a=10&amp;b=20', {asynchronous:true, evalScripts:true, onFailure:function(request){alert(request.responseText)}}); return false;\">Remote outauthor</a>),
link_to_remote("Remote outauthor", :failure => "alert(request.responseText)", :url => { :action => "whatnot", :a => '10', :b => '20' })
+ assert_dom_equal %(<a href=\"#\" onclick=\"new Ajax.Request('http://www.example.com/whatnot', {asynchronous:false, evalScripts:true}); return false;\">Remote outauthor</a>),
+ link_to_remote("Remote outauthor", :url => { :action => "whatnot" }, :type => :synchronous)
end
def test_link_to_remote_html_options
@@ -243,8 +245,12 @@ class PrototypeHelperTest < PrototypeHelperBaseTest
end
def test_update_page
+ old_output_buffer = output_buffer
+
block = Proc.new { |page| page.replace_html('foo', 'bar') }
assert_equal create_generator(&block).to_s, update_page(&block)
+
+ assert_equal old_output_buffer, output_buffer
end
def test_update_page_tag
@@ -283,13 +289,13 @@ class JavaScriptGeneratorTest < PrototypeHelperBaseTest
end
def test_insert_html_with_string
- assert_equal 'new Insertion.Top("element", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");',
+ assert_equal 'Element.insert("element", { top: "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E" });',
@generator.insert_html(:top, 'element', '<p>This is a test</p>')
- assert_equal 'new Insertion.Bottom("element", "\\u003Cp\u003EThis is a test\\u003C/p\u003E");',
+ assert_equal 'Element.insert("element", { bottom: "\\u003Cp\u003EThis is a test\\u003C/p\u003E" });',
@generator.insert_html(:bottom, 'element', '<p>This is a test</p>')
- assert_equal 'new Insertion.Before("element", "\\u003Cp\u003EThis is a test\\u003C/p\u003E");',
+ assert_equal 'Element.insert("element", { before: "\\u003Cp\u003EThis is a test\\u003C/p\u003E" });',
@generator.insert_html(:before, 'element', '<p>This is a test</p>')
- assert_equal 'new Insertion.After("element", "\\u003Cp\u003EThis is a test\\u003C/p\u003E");',
+ assert_equal 'Element.insert("element", { after: "\\u003Cp\u003EThis is a test\\u003C/p\u003E" });',
@generator.insert_html(:after, 'element', '<p>This is a test</p>')
end
@@ -362,8 +368,8 @@ class JavaScriptGeneratorTest < PrototypeHelperBaseTest
@generator.replace_html('baz', '<p>This is a test</p>')
assert_equal <<-EOS.chomp, @generator.to_s
-new Insertion.Top("element", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");
-new Insertion.Bottom("element", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");
+Element.insert("element", { top: "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E" });
+Element.insert("element", { bottom: "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E" });
["foo", "bar"].each(Element.remove);
Element.update("baz", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");
EOS
@@ -425,6 +431,8 @@ Element.update("baz", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");
def test_sortable
assert_equal %(Sortable.create("blah", {onUpdate:function(){new Ajax.Request('http://www.example.com/order', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("blah")})}});),
@generator.sortable('blah', :url => { :action => "order" })
+ assert_equal %(Sortable.create("blah", {onUpdate:function(){new Ajax.Request('http://www.example.com/order', {asynchronous:false, evalScripts:true, parameters:Sortable.serialize("blah")})}});),
+ @generator.sortable('blah', :url => { :action => "order" }, :type => :synchronous)
end
def test_draggable
@@ -435,6 +443,8 @@ Element.update("baz", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");
def test_drop_receiving
assert_equal %(Droppables.add("blah", {onDrop:function(element){new Ajax.Request('http://www.example.com/order', {asynchronous:true, evalScripts:true, parameters:'id=' + encodeURIComponent(element.id)})}});),
@generator.drop_receiving('blah', :url => { :action => "order" })
+ assert_equal %(Droppables.add("blah", {onDrop:function(element){new Ajax.Request('http://www.example.com/order', {asynchronous:false, evalScripts:true, parameters:'id=' + encodeURIComponent(element.id)})}});),
+ @generator.drop_receiving('blah', :url => { :action => "order" }, :type => :synchronous)
end
def test_collection_first_and_last
diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb
index cc5b4900dc..f3c8dbcae9 100644
--- a/actionpack/test/template/render_test.rb
+++ b/actionpack/test/template/render_test.rb
@@ -19,6 +19,10 @@ class ViewRenderTest < Test::Unit::TestCase
assert_equal "Hello world!", @view.render("test/hello_world")
end
+ def test_render_file_at_top_level
+ assert_equal 'Elastica', @view.render('/shared')
+ end
+
def test_render_file_with_full_path
template_path = File.join(File.dirname(__FILE__), '../fixtures/test/hello_world.erb')
assert_equal "Hello world!", @view.render(:file => template_path)
@@ -47,6 +51,24 @@ class ViewRenderTest < Test::Unit::TestCase
assert_equal "only partial", @view.render(:partial => "test/partial_only")
end
+ def test_render_partial_with_format
+ assert_equal 'partial html', @view.render(:partial => 'test/partial')
+ end
+
+ def test_render_partial_at_top_level
+ # file fixtures/_top_level_partial_only.erb (not fixtures/test)
+ assert_equal 'top level partial', @view.render(:partial => '/top_level_partial_only')
+ end
+
+ def test_render_partial_with_format_at_top_level
+ # file fixtures/_top_level_partial.html.erb (not fixtures/test, with format extension)
+ assert_equal 'top level partial html', @view.render(:partial => '/top_level_partial')
+ end
+
+ def test_render_partial_with_locals
+ assert_equal "5", @view.render(:partial => "test/counter", :locals => { :counter_counter => 5 })
+ end
+
def test_render_partial_with_errors
assert_raise(ActionView::TemplateError) { @view.render(:partial => "test/raise") }
end
@@ -54,17 +76,29 @@ class ViewRenderTest < Test::Unit::TestCase
def test_render_partial_collection
assert_equal "Hello: davidHello: mary", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ])
end
-
+
def test_render_partial_collection_as
- assert_equal "david david davidmary mary mary",
+ assert_equal "david david davidmary mary mary",
@view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer)
end
-
+
def test_render_partial_collection_without_as
- assert_equal "local_inspector,local_inspector_counter,object",
+ assert_equal "local_inspector,local_inspector_counter,object",
@view.render(:partial => "test/local_inspector", :collection => [ Customer.new("mary") ])
end
+ def test_render_partial_with_empty_collection_should_return_nil
+ assert_nil @view.render(:partial => "test/customer", :collection => [])
+ end
+
+ def test_render_partial_with_nil_collection_should_return_nil
+ assert_nil @view.render(:partial => "test/customer", :collection => nil)
+ end
+
+ def test_render_partial_with_empty_array_should_return_nil
+ assert_nil @view.render(:partial => [])
+ end
+
# TODO: The reason for this test is unclear, improve documentation
def test_render_partial_and_fallback_to_layout
assert_equal "Before (Josh)\n\nAfter", @view.render(:partial => "test/layout_for_partial", :locals => { :name => "Josh" })
@@ -94,38 +128,18 @@ class ViewRenderTest < Test::Unit::TestCase
assert_equal "Hello, World!", @view.render(:inline => "Hello, World!", :type => :foo)
end
- class CustomHandler < ActionView::TemplateHandler
- def render(template, local_assigns)
- [template.source, local_assigns].inspect
- end
- end
-
- def test_render_inline_with_custom_type
- ActionView::Template.register_template_handler :foo, CustomHandler
- assert_equal '["Hello, World!", {}]', @view.render(:inline => "Hello, World!", :type => :foo)
- end
-
- def test_render_inline_with_locals_and_custom_type
- ActionView::Template.register_template_handler :foo, CustomHandler
- assert_equal '["Hello, <%= name %>!", {:name=>"Josh"}]', @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }, :type => :foo)
- end
-
- class CompilableCustomHandler < ActionView::TemplateHandler
- include ActionView::TemplateHandlers::Compilable
-
- def compile(template)
- "@output_buffer = ''\n" +
- "@output_buffer << 'source: #{template.source.inspect}'\n"
- end
+ CustomHandler = lambda do |template|
+ "@output_buffer = ''\n" +
+ "@output_buffer << 'source: #{template.source.inspect}'\n"
end
def test_render_inline_with_compilable_custom_type
- ActionView::Template.register_template_handler :foo, CompilableCustomHandler
+ ActionView::Template.register_template_handler :foo, CustomHandler
assert_equal 'source: "Hello, World!"', @view.render(:inline => "Hello, World!", :type => :foo)
end
def test_render_inline_with_locals_and_compilable_custom_type
- ActionView::Template.register_template_handler :foo, CompilableCustomHandler
+ ActionView::Template.register_template_handler :foo, CustomHandler
assert_equal 'source: "Hello, <%= name %>!"', @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }, :type => :foo)
end
end
diff --git a/actionpack/test/template/sanitize_helper_test.rb b/actionpack/test/template/sanitize_helper_test.rb
index e5427d9dc1..f715071bbc 100644
--- a/actionpack/test/template/sanitize_helper_test.rb
+++ b/actionpack/test/template/sanitize_helper_test.rb
@@ -11,9 +11,9 @@ class SanitizeHelperTest < ActionView::TestCase
assert_equal "Dont touch me", strip_links("Dont touch me")
assert_equal "<a<a", strip_links("<a<a")
assert_equal "on my mind\nall day long", strip_links("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
- assert_equal "0wn3d", strip_links("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
- assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
- assert_equal "FrrFox", strip_links("<href onlclick='steal()'>FrrFox</a></href>")
+ assert_equal "0wn3d", strip_links("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
+ assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
+ assert_equal "FrrFox", strip_links("<href onlclick='steal()'>FrrFox</a></href>")
assert_equal "My mind\nall <b>day</b> long", strip_links("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
assert_equal "all <b>day</b> long", strip_links("<<a>a href='hello'>all <b>day</b> long<</A>/a>")
end
diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb
index 4999525939..a31d532567 100644
--- a/actionpack/test/template/text_helper_test.rb
+++ b/actionpack/test/template/text_helper_test.rb
@@ -35,8 +35,8 @@ class TextHelperTest < ActionView::TestCase
end
def test_truncate
- assert_equal "Hello World!", truncate("Hello World!", 12)
- assert_equal "Hello Wor...", truncate("Hello World!!", 12)
+ assert_equal "Hello World!", truncate("Hello World!", :length => 12)
+ assert_equal "Hello Wor...", truncate("Hello World!!", :length => 12)
end
def test_truncate_should_use_default_length_of_30
@@ -44,23 +44,29 @@ class TextHelperTest < ActionView::TestCase
assert_equal str[0...27] + "...", truncate(str)
end
+ def test_truncate_with_options_hash
+ assert_equal "This is a string that wil[...]", truncate("This is a string that will go longer then the default truncate length of 30", :omission => "[...]")
+ assert_equal "Hello W...", truncate("Hello World!", :length => 10)
+ assert_equal "Hello[...]", truncate("Hello World!", :omission => "[...]", :length => 10)
+ end
+
if RUBY_VERSION < '1.9.0'
def test_truncate_multibyte
with_kcode 'none' do
- assert_equal "\354\225\210\353\205\225\355...", truncate("\354\225\210\353\205\225\355\225\230\354\204\270\354\232\224", 10)
+ assert_equal "\354\225\210\353\205\225\355...", truncate("\354\225\210\353\205\225\355\225\230\354\204\270\354\232\224", :length => 10)
end
with_kcode 'u' do
assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...",
- truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244", 10)
+ truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244", :length => 10)
end
end
else
def test_truncate_multibyte
assert_equal "\354\225\210\353\205\225\355...",
- truncate("\354\225\210\353\205\225\355\225\230\354\204\270\354\232\224", 10)
+ truncate("\354\225\210\353\205\225\355\225\230\354\204\270\354\232\224", :length => 10)
assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding('UTF-8'),
- truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding('UTF-8'), 10)
+ truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding('UTF-8'), :length => 10)
end
end
@@ -88,7 +94,7 @@ class TextHelperTest < ActionView::TestCase
assert_equal ' ', highlight(' ', 'blank text is returned verbatim')
end
- def test_highlighter_with_regexp
+ def test_highlight_with_regexp
assert_equal(
"This is a <strong class=\"highlight\">beautiful!</strong> morning",
highlight("This is a beautiful! morning", "beautiful!")
@@ -105,10 +111,17 @@ class TextHelperTest < ActionView::TestCase
)
end
- def test_highlighting_multiple_phrases_in_one_pass
+ def test_highlight_with_multiple_phrases_in_one_pass
assert_equal %(<em>wow</em> <em>em</em>), highlight('wow em', %w(wow em), '<em>\1</em>')
end
+ def test_highlight_with_options_hash
+ assert_equal(
+ "This is a <b>beautiful</b> morning, but also a <b>beautiful</b> day",
+ highlight("This is a beautiful morning, but also a beautiful day", "beautiful", :highlighter => '<b>\1</b>')
+ )
+ end
+
def test_excerpt
assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", 5))
assert_equal("This is a...", excerpt("This is a beautiful morning", "this", 5))
@@ -138,6 +151,16 @@ class TextHelperTest < ActionView::TestCase
assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', 'beautiful', 5))
end
+ def test_excerpt_with_options_hash
+ assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", :radius => 5))
+ assert_equal("[...]is a beautiful morn[...]", excerpt("This is a beautiful morning", "beautiful", :omission => "[...]",:radius => 5))
+ assert_equal(
+ "This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome tempera[...]",
+ excerpt("This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome temperatures. So what are you gonna do about it?", "very",
+ :omission => "[...]")
+ )
+ end
+
if RUBY_VERSION < '1.9'
def test_excerpt_with_utf8
with_kcode('u') do
@@ -162,6 +185,10 @@ class TextHelperTest < ActionView::TestCase
assert_equal("my very very\nvery long\nstring\n\nwith another\nline", word_wrap("my very very very long string\n\nwith another line", 15))
end
+ def test_word_wrap_with_options_hash
+ assert_equal("my very very\nvery long\nstring", word_wrap("my very very very long string", :line_width => 15))
+ end
+
def test_pluralization
assert_equal("1 count", pluralize(1, "count"))
assert_equal("2 counts", pluralize(2, "count"))
@@ -285,7 +312,13 @@ class TextHelperTest < ActionView::TestCase
url = "http://api.rubyonrails.com/Foo.html"
email = "fantabulous@shiznadel.ic"
- assert_equal %(<p><a href="#{url}">#{url[0...7]}...</a><br /><a href="mailto:#{email}">#{email[0...7]}...</a><br /></p>), auto_link("<p>#{url}<br />#{email}<br /></p>") { |url| truncate(url, 10) }
+ assert_equal %(<p><a href="#{url}">#{url[0...7]}...</a><br /><a href="mailto:#{email}">#{email[0...7]}...</a><br /></p>), auto_link("<p>#{url}<br />#{email}<br /></p>") { |url| truncate(url, :length => 10) }
+ end
+
+ def test_auto_link_with_options_hash
+ assert_equal 'Welcome to my new blog at <a href="http://www.myblog.com/" class="menu" target="_blank">http://www.myblog.com/</a>. Please e-mail me at <a href="mailto:me@email.com">me@email.com</a>.',
+ auto_link("Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com.",
+ :link => :all, :html => { :class => "menu", :target => "_blank" })
end
def test_cycle_class
diff --git a/actionpack/test/template/translation_helper_test.rb b/actionpack/test/template/translation_helper_test.rb
new file mode 100644
index 0000000000..7b94221ec0
--- /dev/null
+++ b/actionpack/test/template/translation_helper_test.rb
@@ -0,0 +1,28 @@
+require 'abstract_unit'
+
+class TranslationHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::TagHelper
+ include ActionView::Helpers::TranslationHelper
+
+ attr_reader :request
+ uses_mocha 'translation_helper_test' do
+ def setup
+ end
+
+ def test_delegates_to_i18n_setting_the_raise_option
+ I18n.expects(:translate).with(:foo, 'en-US', :raise => true)
+ translate :foo, 'en-US'
+ end
+
+ def test_returns_missing_translation_message_wrapped_into_span
+ expected = '<span class="translation_missing">en-US, foo</span>'
+ assert_equal expected, translate(:foo)
+ end
+
+ def test_delegates_localize_to_i18n
+ @time = Time.utc(2008, 7, 8, 12, 18, 38)
+ I18n.expects(:localize).with(@time)
+ localize @time
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb
index 91d5c6ffb5..85e967ac1c 100644
--- a/actionpack/test/template/url_helper_test.rb
+++ b/actionpack/test/template/url_helper_test.rb
@@ -1,3 +1,4 @@
+# encoding: utf-8
require 'abstract_unit'
RequestMock = Struct.new("Request", :request_uri, :protocol, :host_with_port, :env)
@@ -28,6 +29,16 @@ class UrlHelperTest < ActionView::TestCase
assert_equal "http://www.example.com?a=b&amp;c=d", url_for("http://www.example.com?a=b&amp;c=d")
end
+ def test_url_for_with_back
+ @controller.request = RequestMock.new("http://www.example.com/weblog/show", nil, nil, {'HTTP_REFERER' => 'http://www.example.com/referer'})
+ assert_equal 'http://www.example.com/referer', url_for(:back)
+ end
+
+ def test_url_for_with_back_and_no_referer
+ @controller.request = RequestMock.new("http://www.example.com/weblog/show", nil, nil, {})
+ assert_equal 'javascript:history.back()', url_for(:back)
+ end
+
# todo: missing test cases
def test_button_to_with_straight_url
assert_dom_equal "<form method=\"post\" action=\"http://www.example.com\" class=\"button-to\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", button_to("Hello", "http://www.example.com")
@@ -267,7 +278,11 @@ class UrlHelperTest < ActionView::TestCase
end
def test_mail_to_with_javascript
- assert_dom_equal "<script type=\"text/javascript\">eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript")
+ assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript")
+ end
+
+ def test_mail_to_with_javascript_unicode
+ assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%75%6e%69%63%6f%64%65%40%65%78%61%6d%70%6c%65%2e%63%6f%6d%22%3e%c3%ba%6e%69%63%6f%64%65%3c%2f%61%3e%27%29%3b'))</script>", mail_to("unicode@example.com", "únicode", :encode => "javascript")
end
def test_mail_with_options
@@ -291,8 +306,8 @@ class UrlHelperTest < ActionView::TestCase
assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">&#109;&#101;&#40;&#97;&#116;&#41;&#100;&#111;&#109;&#97;&#105;&#110;&#46;&#99;&#111;&#109;</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)")
assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">My email</a>", mail_to("me@domain.com", "My email", :encode => "hex", :replace_at => "(at)")
assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">&#109;&#101;&#40;&#97;&#116;&#41;&#100;&#111;&#109;&#97;&#105;&#110;&#40;&#100;&#111;&#116;&#41;&#99;&#111;&#109;</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)", :replace_dot => "(dot)")
- assert_dom_equal "<script type=\"text/javascript\">eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)")
- assert_dom_equal "<script type=\"text/javascript\">eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%6d%65%28%61%74%29%64%6f%6d%61%69%6e%28%64%6f%74%29%63%6f%6d%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", nil, :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)")
+ assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)")
+ assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%6d%65%28%61%74%29%64%6f%6d%61%69%6e%28%64%6f%74%29%63%6f%6d%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", nil, :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)")
end
def protect_against_forgery?
@@ -419,7 +434,6 @@ class LinkToUnlessCurrentWithControllerTest < ActionView::TestCase
end
end
-
class Workshop
attr_accessor :id, :new_record
diff --git a/activemodel/Rakefile b/activemodel/Rakefile
index 4b60f8d682..4b60f8d682 100644..100755
--- a/activemodel/Rakefile
+++ b/activemodel/Rakefile
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 95fdd93f65..12295ac799 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,11 @@
*Edge*
+* before_save, before_validation and before_destroy callbacks that return false will now ROLLBACK the transaction. Previously this would have been committed before the processing was aborted. #891 [Xavier Noria]
+
+* Transactional migrations for databases which support them. #834 [divoxx, Adam Wiggins, Tarmo Tänav]
+
+* Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp. #446. [Andrew Stone, Nik Wakelin]
+
* change_column_default preserves the not-null constraint. #617 [Tarmo Tänav]
* Fixed that create database statements would always include "DEFAULT NULL" (Nick Sieger) [#334]
diff --git a/activerecord/README b/activerecord/README
index d68eb28a64..d68eb28a64 100755..100644
--- a/activerecord/README
+++ b/activerecord/README
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 60b17e02b9..983528aff7 100755..100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -30,7 +30,7 @@ desc 'Run mysql, sqlite, and postgresql tests by default'
task :default => :test
desc 'Run mysql, sqlite, and postgresql tests'
-task :test => %w(test_mysql test_sqlite test_sqlite3 test_postgresql)
+task :test => %w(test_mysql test_sqlite3 test_postgresql)
for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase )
Rake::TestTask.new("test_#{adapter}") { |t|
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index d4f7170305..c47ca486c8 100755..100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -21,17 +21,12 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-$:.unshift(File.dirname(__FILE__)) unless
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
-
-unless defined? ActiveSupport
- active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
- if File.exist?(active_support_path)
- $:.unshift active_support_path
- require 'active_support'
- else
- require 'rubygems'
- gem 'activesupport'
+begin
+ require 'active_support'
+rescue LoadError
+ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
+ if File.directory?(activesupport_path)
+ $:.unshift activesupport_path
require 'active_support'
end
end
@@ -56,6 +51,7 @@ require 'active_record/calculations'
require 'active_record/serialization'
require 'active_record/attribute_methods'
require 'active_record/dirty'
+require 'active_record/dynamic_finder_match'
ActiveRecord::Base.class_eval do
extend ActiveRecord::QueryCache
@@ -80,3 +76,8 @@ end
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/schema_dumper'
+
+I18n.backend.populate do
+ I18n.load_translations File.dirname(__FILE__) + '/active_record/locale/en-US.yml'
+end
+
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index 174ec95de2..61fa34ac39 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -34,7 +34,7 @@ module ActiveRecord
class_to_reflection = {}
# Not all records have the same class, so group then preload
# group on the reflection itself so that if various subclass share the same association then we do not split them
- # unncessarily
+ # unnecessarily
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
send("preload_#{reflection.macro}_association", records, reflection, preload_options)
@@ -51,9 +51,7 @@ module ActiveRecord
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
- association_proxy = parent_record.send(reflection_name)
- association_proxy.loaded
- association_proxy.target = associated_record
+ parent_record.send("set_#{reflection_name}_target", associated_record)
end
end
@@ -112,8 +110,8 @@ module ActiveRecord
def preload_has_one_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map(records)
options = reflection.options
+ records.each {|record| record.send("set_#{reflection.name}_target", nil)}
if options[:through]
- records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded}
through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name
@@ -126,8 +124,6 @@ module ActiveRecord
end
end
else
- records.each {|record| record.send("set_#{reflection.name}_target", nil)}
-
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
end
end
@@ -252,7 +248,7 @@ module ActiveRecord
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.name.demodulize}'"
+ 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}'"
else
foreign_key = reflection.primary_key_name
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)"
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 7ad7802cbc..f915daafba 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -73,6 +73,7 @@ module ActiveRecord
end
end
+ # See ActiveRecord::Associations::ClassMethods for documentation.
module Associations # :nodoc:
def self.included(base)
base.extend(ClassMethods)
@@ -150,6 +151,7 @@ module ActiveRecord
# #others.destroy_all | X | X | X
# #others.find(*args) | X | X | X
# #others.find_first | X | |
+ # #others.exist? | X | X | X
# #others.uniq | X | X | X
# #others.reset | X | X | X
#
@@ -582,12 +584,13 @@ module ActiveRecord
# has_many :clients
# end
#
- # class Company < ActiveRecord::Base; end
+ # class Client < ActiveRecord::Base; end
# end
# end
#
- # When Firm#clients is called, it will in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
- # with a class in another module scope, this can be done by specifying the complete class name. Example:
+ # When <tt>Firm#clients</tt> is called, it will in turn call <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>.
+ # If you want to associate with a class in another module scope, this can be done by specifying the complete class name.
+ # Example:
#
# module MyApplication
# module Business
@@ -611,31 +614,53 @@ module ActiveRecord
# All of the association macros can be specialized through options. This makes cases more complex than the simple and guessable ones
# possible.
module ClassMethods
- # Adds the following methods for retrieval and query of collections of associated objects:
- # +collection+ is replaced with the symbol passed as the first argument, so
- # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
- # * <tt>collection(force_reload = false)</tt> - Returns an array of all the associated objects.
+ # Specifies a one-to-many association. The following methods for retrieval and query of
+ # collections of associated objects will be added:
+ #
+ # [collection(force_reload = false)]
+ # Returns an array of all the associated objects.
# An empty array is returned if none are found.
- # * <tt>collection<<(object, ...)</tt> - Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
- # * <tt>collection.delete(object, ...)</tt> - Removes one or more objects from the collection by setting their foreign keys to +NULL+.
+ # [collection<<(object, ...)]
+ # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
+ # [collection.delete(object, ...)]
+ # Removes one or more objects from the collection by setting their foreign keys to +NULL+.
# This will also destroy the objects if they're declared as +belongs_to+ and dependent on this model.
- # * <tt>collection=objects</tt> - Replaces the collections content by deleting and adding objects as appropriate.
- # * <tt>collection_singular_ids</tt> - Returns an array of the associated objects' ids
- # * <tt>collection_singular_ids=ids</tt> - Replace the collection with the objects identified by the primary keys in +ids+
- # * <tt>collection.clear</tt> - Removes every object from the collection. This destroys the associated objects if they
- # are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the database if <tt>:dependent => :delete_all</tt>,
- # otherwise sets their foreign keys to +NULL+.
- # * <tt>collection.empty?</tt> - Returns +true+ if there are no associated objects.
- # * <tt>collection.size</tt> - Returns the number of associated objects.
- # * <tt>collection.find</tt> - Finds an associated object according to the same rules as Base.find.
- # * <tt>collection.build(attributes = {}, ...)</tt> - Returns one or more new objects of the collection type that have been instantiated
- # with +attributes+ and linked to this object through a foreign key, but have not yet been saved. *Note:* This only works if an
- # associated object already exists, not if it's +nil+!
- # * <tt>collection.create(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
- # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
- # *Note:* This only works if an associated object already exists, not if it's +nil+!
+ # [collection=objects]
+ # Replaces the collections content by deleting and adding objects as appropriate.
+ # [collection_singular_ids]
+ # Returns an array of the associated objects' ids
+ # [collection_singular_ids=ids]
+ # Replace the collection with the objects identified by the primary keys in +ids+
+ # [collection.clear]
+ # Removes every object from the collection. This destroys the associated objects if they
+ # are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the
+ # database if <tt>:dependent => :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
+ # [collection.empty?]
+ # Returns +true+ if there are no associated objects.
+ # [collection.size]
+ # Returns the number of associated objects.
+ # [collection.find(...)]
+ # Finds an associated object according to the same rules as ActiveRecord::Base.find.
+ # [collection.exist?(...)]
+ # Checks whether an associated object with the given conditions exists.
+ # Uses the same rules as ActiveRecord::Base.exists?.
+ # [collection.build(attributes = {}, ...)]
+ # Returns one or more new objects of the collection type that have been instantiated
+ # with +attributes+ and linked to this object through a foreign key, but have not yet
+ # been saved. <b>Note:</b> This only works if an associated object already exists, not if
+ # it's +nil+!
+ # [collection.create(attributes = {})]
+ # Returns a new object of the collection type that has been instantiated
+ # with +attributes+, linked to this object through a foreign key, and that has already
+ # been saved (if it passed the validation). <b>Note:</b> This only works if an associated
+ # object already exists, not if it's +nil+!
+ #
+ # (*Note*: +collection+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.)
#
- # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
+ # === Example
+ #
+ # A Firm class declares <tt>has_many :clients</tt>, which will add:
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>)
# * <tt>Firm#clients<<</tt>
# * <tt>Firm#clients.delete</tt>
@@ -646,54 +671,77 @@ module ActiveRecord
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
+ # * <tt>Firm#clients.exist?(:name => 'ACME')</tt> (similar to <tt>Client.exist?(:name => 'ACME', :firm_id => firm.id)</tt>)
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
- # Options are:
- # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
+ # === Supported options
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_many :products</tt> will by default be linked to the Product class, but
# if the real class name is SpecialProduct, you'll have to specify it with this option.
- # * <tt>:conditions</tt> - Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
+ # [:conditions]
+ # Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
# SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from the association are scoped if a hash
# is used. <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
# or <tt>@blog.posts.build</tt>.
- # * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
+ # [:order]
+ # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
# such as <tt>last_name, first_name DESC</tt>.
- # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
+ # [:foreign_key]
+ # Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ association will use "person_id"
# as the default <tt>:foreign_key</tt>.
- # * <tt>:primary_key</tt> - Specify the method that returns the primary key used for the association. By default this is +id+.
- # * <tt>:dependent</tt> - If set to <tt>:destroy</tt> all the associated objects are destroyed
+ # [:primary_key]
+ # Specify the method that returns the primary key used for the association. By default this is +id+.
+ # [:dependent]
+ # If set to <tt>:destroy</tt> all the associated objects are destroyed
# alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated
# objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. *Warning:* This option is ignored when also using
# the <tt>:through</tt> option.
- # * <tt>:finder_sql</tt> - Specify a complete SQL statement to fetch the association. This is a good way to go for complex
+ # [:finder_sql]
+ # Specify a complete SQL statement to fetch the association. This is a good way to go for complex
# associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
- # * <tt>:counter_sql</tt> - Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
+ # [:counter_sql]
+ # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
# specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
- # * <tt>:extend</tt> - Specify a named module for extending the proxy. See "Association extensions".
- # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when the collection is loaded.
- # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
- # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
- # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
- # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join
+ # [:extend]
+ # Specify a named module for extending the proxy. See "Association extensions".
+ # [:include]
+ # Specify second-order associations that should be eager loaded when the collection is loaded.
+ # [:group]
+ # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
+ # [:limit]
+ # An integer determining the limit on the number of rows that should be returned.
+ # [:offset]
+ # An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
+ # [:select]
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
- # * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>).
- # * <tt>:through</tt> - Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
+ # [:as]
+ # Specifies a polymorphic interface (See <tt>belongs_to</tt>).
+ # [:through]
+ # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>
# or <tt>has_many</tt> association on the join model.
- # * <tt>:source</tt> - Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
+ # [:source]
+ # Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
# inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
- # * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
+ # [:source_type]
+ # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
# association is a polymorphic +belongs_to+.
- # * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
- # * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
- # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. true by default.
- # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
- #
+ # [:uniq]
+ # If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
+ # [:readonly]
+ # 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 <tt>ActiveRecord::Base#attr_accessible</tt>).
+
# Option examples:
# has_many :comments, :order => "posted_on"
# has_many :comments, :include => :author
@@ -724,58 +772,91 @@ module ActiveRecord
end
end
- # Adds the following methods for retrieval and query of a single associated object:
- # +association+ is replaced with the symbol passed as the first argument, so
- # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
- # * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found.
- # * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, sets it as the foreign key,
+ # Specifies a one-to-one association with another class. This method should only be used
+ # if the other class contains the foreign key. If the current class contains the foreign key,
+ # then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview
+ # on when to use has_one and when to use belongs_to.
+ #
+ # The following methods for retrieval and query of a single associated object will be added:
+ #
+ # [association(force_reload = false)]
+ # Returns the associated object. +nil+ is returned if none is found.
+ # [association=(associate)]
+ # Assigns the associate object, extracts the primary key, sets it as the foreign key,
# and saves the associate object.
- # * <tt>association.nil?</tt> - Returns +true+ if there is no associated object.
- # * <tt>build_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
- # with +attributes+ and linked to this object through a foreign key, but has not yet been saved. Note: This ONLY works if
- # an association already exists. It will NOT work if the association is +nil+.
- # * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
- # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
+ # [association.nil?]
+ # Returns +true+ if there is no associated object.
+ # [build_association(attributes = {})]
+ # Returns a new object of the associated type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key, but has not
+ # yet been saved. <b>Note:</b> This ONLY works if an association already exists.
+ # It will NOT work if the association is +nil+.
+ # [create_association(attributes = {})]
+ # Returns a new object of the associated type that has been instantiated
+ # with +attributes+, linked to this object through a foreign key, and that
+ # has already been saved (if it passed the validation).
#
- # Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
+ # (+association+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.)
+ #
+ # === Example
+ #
+ # An Account class declares <tt>has_one :beneficiary</tt>, which will add:
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find(:first, :conditions => "account_id = #{id}")</tt>)
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
# * <tt>Account#beneficiary.nil?</tt>
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
#
+ # === Options
+ #
# The declaration can also include an options hash to specialize the behavior of the association.
#
# Options are:
- # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
# if the real class name is Person, you'll have to specify it with this option.
- # * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+
+ # [:conditions]
+ # Specify the conditions that the associated object must meet in order to be included as a +WHERE+
# SQL fragment, such as <tt>rank = 5</tt>.
- # * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
+ # [:order]
+ # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
# such as <tt>last_name, first_name DESC</tt>.
- # * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
+ # [:dependent]
+ # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. If set to <tt>:nullify</tt>, the associated
# object's foreign key is set to +NULL+. Also, association is assigned.
- # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
+ # [:foreign_key]
+ # Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association will use "person_id"
# as the default <tt>:foreign_key</tt>.
- # * <tt>:primary_key</tt> - Specify the method that returns the primary key used for the association. By default this is +id+.
- # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded.
- # * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>).
- # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
+ # [:primary_key]
+ # Specify the method that returns the primary key used for the association. By default this is +id+.
+ # [:include]
+ # Specify second-order associations that should be eager loaded when this object is loaded.
+ # [:as]
+ # Specifies a polymorphic interface (See <tt>belongs_to</tt>).
+ # [:select]
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
- # * <tt>:through</tt>: Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
+ # [:through]
+ # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a
# <tt>has_one</tt> or <tt>belongs_to</tt> association on the join model.
- # * <tt>:source</tt> - Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be
+ # [:source]
+ # Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be
# inferred from the association. <tt>has_one :favorite, :through => :favorites</tt> will look for a
# <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
- # * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
+ # [:source_type]
+ # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
# association is a polymorphic +belongs_to+.
- # * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
- # * <tt>:validate</tt> - If false, don't validate the associated object when saving the parent object. +false+ by default.
- # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
+ # [:readonly]
+ # 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 <tt>ActiveRecord::Base#attr_accessible</tt>).
#
# Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
@@ -815,18 +896,34 @@ module ActiveRecord
end
end
- # Adds the following methods for retrieval and query for a single associated object for which this object holds an id:
- # +association+ is replaced with the symbol passed as the first argument, so
- # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
- # * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found.
- # * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, and sets it as the foreign key.
- # * <tt>association.nil?</tt> - Returns +true+ if there is no associated object.
- # * <tt>build_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
+ # Specifies a one-to-one association with another class. This method should only be used
+ # if this class contains the foreign key. If the other class contains the foreign key,
+ # then you should use +has_one+ instead. See also ActiveRecord::Associations::ClassMethods's overview
+ # on when to use +has_one+ and when to use +belongs_to+.
+ #
+ # Methods will be added for retrieval and query for a single associated object, for which
+ # this object holds an id:
+ #
+ # [association(force_reload = false)]
+ # Returns the associated object. +nil+ is returned if none is found.
+ # [association=(associate)]
+ # Assigns the associate object, extracts the primary key, and sets it as the foreign key.
+ # [association.nil?]
+ # Returns +true+ if there is no associated object.
+ # [build_association(attributes = {})]
+ # Returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
- # * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
- # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
+ # [create_association(attributes = {})]
+ # Returns a new object of the associated type that has been instantiated
+ # with +attributes+, linked to this object through a foreign key, and that
+ # has already been saved (if it passed the validation).
+ #
+ # (+association+ is replaced with the symbol passed as the first argument, so
+ # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.)
#
- # Example: A Post class declares <tt>belongs_to :author</tt>, which will add:
+ # === Example
+ #
+ # A Post class declares <tt>belongs_to :author</tt>, which will add:
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
@@ -835,23 +932,30 @@ module ActiveRecord
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
- # Options are:
- # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
+ # === Options
+ #
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_one :author</tt> will by default be linked to the Author class, but
# if the real class name is Person, you'll have to specify it with this option.
- # * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+
+ # [:conditions]
+ # Specify the conditions that the associated object must meet in order to be included as a +WHERE+
# SQL fragment, such as <tt>authorized = 1</tt>.
- # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
+ # [:select]
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
- # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
+ # [:foreign_key]
+ # Specify the foreign key used for the association. By default this is guessed to be the name
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use
# "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt>
# will use a foreign key of "favorite_person_id".
- # * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
+ # [:dependent]
+ # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when
# <tt>belongs_to</tt> is used in conjunction with a <tt>has_many</tt> relationship on another class because of the potential to leave
# orphaned records behind.
- # * <tt>:counter_cache</tt> - Caches the number of belonging objects on the associate class through the use of +increment_counter+
+ # [:counter_cache]
+ # Caches the number of belonging objects on the associate class through the use of +increment_counter+
# and +decrement_counter+. The counter cache is incremented when an object of this class is created and decremented when it's
# destroyed. This requires that a column named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
# is used on the associate class (such as a Post class). You can also specify a custom counter cache column by providing
@@ -859,13 +963,18 @@ module ActiveRecord
# When creating a counter cache column, the database statement or migration must specify a default value of <tt>0</tt>, failing to do
# this results in a counter with +NULL+ value, which will never increment.
# Note: Specifying a counter cache will add it to that model's list of readonly attributes using +attr_readonly+.
- # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded.
- # * <tt>:polymorphic</tt> - Specify this association is a polymorphic association by passing +true+.
+ # [:include]
+ # Specify second-order associations that should be eager loaded when this object is loaded.
+ # [:polymorphic]
+ # Specify this association is a polymorphic association by passing +true+.
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
- # * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
- # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +false+ by default.
- # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
+ # [:readonly]
+ # 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 <tt>ActiveRecord::Base#attr_accessible</tt>).
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
@@ -951,8 +1060,9 @@ module ActiveRecord
configure_dependency_for_belongs_to(reflection)
end
- # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
- # an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
+ # Specifies a many-to-many relationship with another class. This associates two classes via an
+ # intermediate join table. Unless the join table is explicitly specified as an option, it is
+ # guessed using the lexical order of the class names. So a join between Developer and Project
# will give the default join table name of "developers_projects" because "D" outranks "P". Note that this precedence
# is calculated using the <tt><</tt> operator for String. This means that if the strings are of different lengths,
# and the strings are equal when compared up to the shortest length, then the longer string is considered of higher
@@ -967,28 +1077,48 @@ module ActiveRecord
# associations with attributes to a real join model (see introduction).
#
# Adds the following methods for retrieval and query:
- # +collection+ is replaced with the symbol passed as the first argument, so
- # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
- # * <tt>collection(force_reload = false)</tt> - Returns an array of all the associated objects.
+ #
+ # [collection(force_reload = false)]
+ # Returns an array of all the associated objects.
# An empty array is returned if none are found.
- # * <tt>collection<<(object, ...)</tt> - Adds one or more objects to the collection by creating associations in the join table
+ # [collection<<(object, ...)]
+ # Adds one or more objects to the collection by creating associations in the join table
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
- # * <tt>collection.delete(object, ...)</tt> - Removes one or more objects from the collection by removing their associations from the join table.
+ # [collection.delete(object, ...)]
+ # Removes one or more objects from the collection by removing their associations from the join table.
# This does not destroy the objects.
- # * <tt>collection=objects</tt> - Replaces the collection's content by deleting and adding objects as appropriate.
- # * <tt>collection_singular_ids</tt> - Returns an array of the associated objects' ids.
- # * <tt>collection_singular_ids=ids</tt> - Replace the collection by the objects identified by the primary keys in +ids+.
- # * <tt>collection.clear</tt> - Removes every object from the collection. This does not destroy the objects.
- # * <tt>collection.empty?</tt> - Returns +true+ if there are no associated objects.
- # * <tt>collection.size</tt> - Returns the number of associated objects.
- # * <tt>collection.find(id)</tt> - Finds an associated object responding to the +id+ and that
+ # [collection=objects]
+ # Replaces the collection's content by deleting and adding objects as appropriate.
+ # [collection_singular_ids]
+ # Returns an array of the associated objects' ids.
+ # [collection_singular_ids=ids]
+ # Replace the collection by the objects identified by the primary keys in +ids+.
+ # [collection.clear]
+ # Removes every object from the collection. This does not destroy the objects.
+ # [collection.empty?]
+ # Returns +true+ if there are no associated objects.
+ # [collection.size]
+ # Returns the number of associated objects.
+ # [collection.find(id)]
+ # Finds an associated object responding to the +id+ and that
# meets the condition that it has to be associated with this object.
- # * <tt>collection.build(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
+ # Uses the same rules as ActiveRecord::Base.find.
+ # [collection.exist?(...)]
+ # Checks whether an associated object with the given conditions exists.
+ # Uses the same rules as ActiveRecord::Base.exists?.
+ # [collection.build(attributes = {})]
+ # Returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
- # * <tt>collection.create(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
+ # [collection.create(attributes = {})]
+ # Returns a new object of the collection type that has been instantiated
# with +attributes+, linked to this object through the join table, and that has already been saved (if it passed the validation).
#
- # Example: A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
+ # (+collection+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.)
+ #
+ # === Example
+ #
+ # A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
# * <tt>Developer#projects</tt>
# * <tt>Developer#projects<<</tt>
# * <tt>Developer#projects.delete</tt>
@@ -999,45 +1129,66 @@ module ActiveRecord
# * <tt>Developer#projects.empty?</tt>
# * <tt>Developer#projects.size</tt>
# * <tt>Developer#projects.find(id)</tt>
+ # * <tt>Developer#clients.exist?(...)</tt>
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>)
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>)
# The declaration may include an options hash to specialize the behavior of the association.
#
- # Options are:
- # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
+ # === Options
+ #
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
# Project class, but if the real class name is SuperProject, you'll have to specify it with this option.
- # * <tt>:join_table</tt> - Specify the name of the join table if the default based on lexical order isn't what you want.
- # WARNING: If you're overwriting the table name of either class, the +table_name+ method MUST be declared underneath any
- # +has_and_belongs_to_many+ declaration in order to work.
- # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
+ # [:join_table]
+ # Specify the name of the join table if the default based on lexical order isn't what you want.
+ # <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method
+ # MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work.
+ # [:foreign_key]
+ # Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_and_belongs_to_many+ association
# will use "person_id" as the default <tt>:foreign_key</tt>.
- # * <tt>:association_foreign_key</tt> - Specify the association foreign key used for the association. By default this is
+ # [:association_foreign_key]
+ # Specify the association foreign key used for the association. By default this is
# guessed to be the name of the associated class in lower-case and "_id" suffixed. So if the associated class is Project,
# the +has_and_belongs_to_many+ association will use "project_id" as the default <tt>:association_foreign_key</tt>.
- # * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+
+ # [:conditions]
+ # Specify the conditions that the associated object must meet in order to be included as a +WHERE+
# SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are scoped if a hash is used.
# <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
# or <tt>@blog.posts.build</tt>.
- # * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
+ # [:order]
+ # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
# such as <tt>last_name, first_name DESC</tt>
- # * <tt>:uniq</tt> - If true, duplicate associated objects will be ignored by accessors and query methods.
- # * <tt>:finder_sql</tt> - Overwrite the default generated SQL statement used to fetch the association with a manual statement
- # * <tt>:delete_sql</tt> - Overwrite the default generated SQL statement used to remove links between the associated
+ # [:uniq]
+ # If true, duplicate associated objects will be ignored by accessors and query methods.
+ # [:finder_sql]
+ # Overwrite the default generated SQL statement used to fetch the association with a manual statement
+ # [:delete_sql]
+ # Overwrite the default generated SQL statement used to remove links between the associated
# classes with a manual statement.
- # * <tt>:insert_sql</tt> - Overwrite the default generated SQL statement used to add links between the associated classes
+ # [:insert_sql]
+ # Overwrite the default generated SQL statement used to add links between the associated classes
# with a manual statement.
- # * <tt>:extend</tt> - Anonymous module for extending the proxy, see "Association extensions".
- # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when the collection is loaded.
- # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
- # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
- # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
- # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
+ # [:extend]
+ # Anonymous module for extending the proxy, see "Association extensions".
+ # [:include]
+ # Specify second-order associations that should be eager loaded when the collection is loaded.
+ # [:group]
+ # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
+ # [:limit]
+ # An integer determining the limit on the number of rows that should be returned.
+ # [:offset]
+ # An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
+ # [:select]
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
- # * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
- # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +true+ by default.
- # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
+ # [:readonly]
+ # 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 <tt>ActiveRecord::Base#attr_accessible</tt>).
#
# Option examples:
# has_and_belongs_to_many :projects
@@ -1528,19 +1679,19 @@ module ActiveRecord
else all << cond
end
end
- conditions.join(' ').scan(/([\.\w]+).?\./).flatten
+ conditions.join(' ').scan(/([\.a-zA-Z_]+).?\./).flatten
end
def order_tables(options)
order = [options[:order], scope(:find, :order) ].join(", ")
return [] unless order && order.is_a?(String)
- order.scan(/([\.\w]+).?\./).flatten
+ order.scan(/([\.a-zA-Z_]+).?\./).flatten
end
def selects_tables(options)
select = options[:select]
return [] unless select && select.is_a?(String)
- select.scan(/"?([\.\w]+)"?.?\./).flatten
+ select.scan(/"?([\.a-zA-Z_]+)"?.?\./).flatten
end
# Checks if the conditions reference a table other than the current model table
@@ -1742,6 +1893,7 @@ module ActiveRecord
collection.target.push(association)
when :has_one
return if record.id.to_s != join.parent.record_id(row).to_s
+ return if record.instance_variable_defined?("@#{join.reflection.name}")
association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
record.send("set_#{join.reflection.name}_target", association)
when :belongs_to
@@ -1823,7 +1975,7 @@ module ActiveRecord
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
end
- if reflection.macro == :has_many && reflection.options[:through]
+ if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
end
end
@@ -1847,7 +1999,7 @@ module ActiveRecord
]
when :has_many, :has_one
case
- when reflection.macro == :has_many && reflection.options[:through]
+ when reflection.options[:through]
through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
@@ -1883,7 +2035,7 @@ module ActiveRecord
jt_sti_extra = " AND %s.%s = %s" % [
connection.quote_table_name(aliased_join_table_name),
connection.quote_column_name(through_reflection.active_record.inheritance_column),
- through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)]
+ through_reflection.klass.quote_value(through_reflection.klass.sti_name)]
end
when :belongs_to
first_key = primary_key
@@ -1948,10 +2100,8 @@ module ActiveRecord
else
""
end || ''
- join << %(AND %s.%s = %s ) % [
- connection.quote_table_name(aliased_table_name),
- connection.quote_column_name(klass.inheritance_column),
- klass.quote_value(klass.name.demodulize)] unless klass.descends_from_active_record?
+ join << %(AND %s) % [
+ klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record?
[through_reflection, reflection].each do |ref|
join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions]
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index a28be9eed1..9061037b39 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -344,7 +344,7 @@ module ActiveRecord
callback(:before_add, record)
yield(record) if block_given?
@target ||= [] unless loaded?
- @target << record
+ @target << record unless @reflection.options[:uniq] && @target.include?(record)
callback(:after_add, record)
record
end
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index 77fc827e11..99b8748a48 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -131,10 +131,6 @@ module ActiveRecord
records.map { |record| record.quoted_id }.join(',')
end
- def interpolate_sql_options!(options, *keys)
- keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
- end
-
def interpolate_sql(sql, record = nil)
@owner.send(:interpolate_sql, sql, record)
end
@@ -217,7 +213,7 @@ module ActiveRecord
# Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
def flatten_deeper(array)
- array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
+ array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
end
def owner_quoted_id
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 7c28cbdd07..7c28cbdd07 100755..100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index d8146daa54..d8146daa54 100755..100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index d516d54151..e7e433b6b6 100644
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -70,10 +70,8 @@ module ActiveRecord
end
def construct_sql
- interpolate_sql_options!(@reflection.options, :finder_sql)
-
if @reflection.options[:finder_sql]
- @finder_sql = @reflection.options[:finder_sql]
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
else
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
@finder_sql << " AND (#{conditions})" if conditions
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index e6fa15c173..ce62127505 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -35,8 +35,11 @@ module ActiveRecord
else
@reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
end
-
- @target = [] and loaded if count == 0
+
+ # If there's nothing in the database and @target has no new records
+ # we are certain the current target is an empty array. This is a
+ # documented side-effect of the method that may avoid an extra SELECT.
+ @target ||= [] and loaded if count == 0
if @reflection.options[:limit]
count = [ @reflection.options[:limit], count ].min
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index e1bfff5923..24b02efc35 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -237,7 +237,7 @@ module ActiveRecord
end
def build_sti_condition
- "#{@reflection.through_reflection.quoted_table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.sti_name)}"
+ @reflection.through_reflection.klass.send(:type_condition)
end
alias_method :sql_conditions, :conditions
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index fdc0fa52c9..18733255d2 100755..100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -21,8 +21,8 @@ module ActiveRecord
def replace(obj, dont_save = false)
load_target
- unless @target.nil?
- if dependent? && !dont_save && @target != obj
+ unless @target.nil? || @target == obj
+ if dependent? && !dont_save
@target.destroy unless @target.new_record?
@owner.clear_association_cache
else
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 962c2b36d9..5c30f80555 100755..100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -6,7 +6,7 @@ module ActiveRecord #:nodoc:
class ActiveRecordError < StandardError
end
- # Raised when the single-table inheritance mechanism failes to locate the subclass
+ # Raised when the single-table inheritance mechanism fails to locate the subclass
# (for example due to improper usage of column that +inheritance_column+ points to).
class SubclassNotFound < ActiveRecordError #:nodoc:
end
@@ -83,8 +83,33 @@ module ActiveRecord #:nodoc:
class ReadOnlyRecord < ActiveRecordError
end
- # Used by Active Record transaction mechanism to distinguish rollback from other exceptional situations.
- # You can use it to roll your transaction back explicitly in the block passed to +transaction+ method.
+ # ActiveRecord::Transactions::ClassMethods.transaction uses this exception
+ # to distinguish a deliberate rollback from other exceptional situations.
+ # Normally, raising an exception will cause the +transaction+ method to rollback
+ # the database transaction *and* pass on the exception. But if you raise an
+ # ActiveRecord::Rollback exception, then the database transaction will be rolled back,
+ # without passing on the exception.
+ #
+ # For example, you could do this in your controller to rollback a transaction:
+ #
+ # class BooksController < ActionController::Base
+ # def create
+ # Book.transaction do
+ # book = Book.new(params[:book])
+ # book.save!
+ # if today_is_friday?
+ # # The system must fail on Friday so that our support department
+ # # won't be out of job. We silently rollback this transaction
+ # # without telling the user.
+ # raise ActiveRecord::Rollback, "Call tech support!"
+ # end
+ # end
+ # # ActiveRecord::Rollback is the only exception that won't be passed on
+ # # by ActiveRecord::Base.transaction, so this line will still be reached
+ # # even on Friday.
+ # redirect_to root_url
+ # end
+ # end
class Rollback < ActiveRecordError
end
@@ -97,7 +122,11 @@ module ActiveRecord #:nodoc:
class MissingAttributeError < NoMethodError
end
- # Raised when an error occured while doing a mass assignment to an attribute through the
+ # Raised when unknown attributes are supplied via mass assignment.
+ class UnknownAttributeError < NoMethodError
+ end
+
+ # Raised when an error occurred while doing a mass assignment to an attribute through the
# <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the
# offending attribute.
class AttributeAssignmentError < ActiveRecordError
@@ -271,7 +300,7 @@ module ActiveRecord #:nodoc:
# # Now 'Bob' exist and is an 'admin'
# User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true }
#
- # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without saving it first. Protected attributes won't be setted unless they are given in a block. For example:
+ # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without saving it first. Protected attributes won't be set unless they are given in a block. For example:
#
# # No 'Winter' tag exists
# winter = Tag.find_or_initialize_by_name("Winter")
@@ -439,6 +468,10 @@ module ActiveRecord #:nodoc:
cattr_accessor :schema_format , :instance_writer => false
@@schema_format = :ruby
+ # Specify whether or not to use timestamps for migration numbers
+ cattr_accessor :timestamped_migrations , :instance_writer => false
+ @@timestamped_migrations = true
+
# Determine whether to store the full constant name including namespace when using STI
superclass_delegating_accessor :store_full_sti_class
self.store_full_sti_class = false
@@ -724,8 +757,7 @@ module ActiveRecord #:nodoc:
# ==== Attributes
#
# * +updates+ - A String of column and value pairs that will be set on any records that match conditions.
- # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
- # See conditions in the intro for more info.
+ # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info.
# * +options+ - Additional options are <tt>:limit</tt> and/or <tt>:order</tt>, see the examples for usage.
#
# ==== Examples
@@ -828,7 +860,7 @@ module ActiveRecord #:nodoc:
def update_counters(id, counters)
updates = counters.inject([]) { |list, (counter_name, increment)|
sign = increment < 0 ? "-" : "+"
- list << "#{connection.quote_column_name(counter_name)} = #{connection.quote_column_name(counter_name)} #{sign} #{increment.abs}"
+ list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}"
}.join(", ")
update_all(updates, "#{connection.quote_column_name(primary_key)} = #{quote_value(id)}")
end
@@ -1188,11 +1220,46 @@ module ActiveRecord #:nodoc:
subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
end
+ def self_and_descendents_from_active_record#nodoc:
+ klass = self
+ classes = [klass]
+ while klass != klass.base_class
+ classes << klass = klass.superclass
+ end
+ classes
+ rescue
+ # OPTIMIZE this rescue is to fix this test: ./test/cases/reflection_test.rb:56:in `test_human_name_for_column'
+ # Appearantly the method base_class causes some trouble.
+ # It now works for sure.
+ [self]
+ end
+
# Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
# Person.human_attribute_name("first_name") # => "First name"
- # Deprecated in favor of just calling "first_name".humanize
- def human_attribute_name(attribute_key_name) #:nodoc:
- attribute_key_name.humanize
+ # This used to be depricated in favor of humanize, but is now preferred, because it automatically uses the I18n
+ # module now.
+ # Specify +options+ with additional translating options.
+ def human_attribute_name(attribute_key_name, options = {})
+ defaults = self_and_descendents_from_active_record.map do |klass|
+ :"#{klass.name.underscore}.#{attribute_key_name}"
+ end
+ defaults << options[:default] if options[:default]
+ defaults.flatten!
+ defaults << attribute_key_name.humanize
+ options[:count] ||= 1
+ I18n.translate(defaults.shift, options.merge(:default => defaults, :scope => [:activerecord, :attributes]))
+ end
+
+ # Transform the modelname into a more humane format, using I18n.
+ # Defaults to the basic humanize method.
+ # Default scope of the translation is activerecord.models
+ # Specify +options+ with additional translating options.
+ def human_name(options = {})
+ defaults = self_and_descendents_from_active_record.map do |klass|
+ :"#{klass.name.underscore}"
+ end
+ defaults << self.name.humanize
+ I18n.translate(defaults.shift, {:scope => [:activerecord, :models], :count => 1, :default => defaults}.merge(options))
end
# True if this isn't a concrete subclass needing a STI type condition.
@@ -1287,8 +1354,8 @@ module ActiveRecord #:nodoc:
end
def respond_to?(method_id, include_private = false)
- if match = matches_dynamic_finder?(method_id) || matches_dynamic_finder_with_initialize_or_create?(method_id)
- return true if all_attributes_exists?(extract_attribute_names_from_match(match))
+ if match = DynamicFinderMatch.match(method_id)
+ return true if all_attributes_exists?(match.attribute_names)
end
super
end
@@ -1577,10 +1644,11 @@ module ActiveRecord #:nodoc:
sql << "WHERE #{merged_conditions} " unless merged_conditions.blank?
end
- def type_condition
+ def type_condition(table_alias=nil)
+ quoted_table_alias = self.connection.quote_table_name(table_alias || table_name)
quoted_inheritance_column = connection.quote_column_name(inheritance_column)
- type_condition = subclasses.inject("#{quoted_table_name}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
- condition << "OR #{quoted_table_name}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
+ type_condition = subclasses.inject("#{quoted_table_alias}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
+ condition << "OR #{quoted_table_alias}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
end
" (#{type_condition}) "
@@ -1606,88 +1674,67 @@ module ActiveRecord #:nodoc:
# Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future
# attempts to use it do not run through method_missing.
def method_missing(method_id, *arguments)
- if match = matches_dynamic_finder?(method_id)
- finder = determine_finder(match)
-
- attribute_names = extract_attribute_names_from_match(match)
+ if match = DynamicFinderMatch.match(method_id)
+ attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)
-
- self.class_eval %{
- def self.#{method_id}(*args)
- options = args.extract_options!
- attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
- finder_options = { :conditions => attributes }
- validate_find_options(options)
- set_readonly_option!(options)
-
- if options[:conditions]
- with_scope(:find => finder_options) do
- ActiveSupport::Deprecation.silence { send(:#{finder}, options) }
+ if match.finder?
+ finder = match.finder
+ bang = match.bang?
+ self.class_eval %{
+ def self.#{method_id}(*args)
+ options = args.extract_options!
+ attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
+ finder_options = { :conditions => attributes }
+ validate_find_options(options)
+ set_readonly_option!(options)
+
+ #{'result = ' if bang}if options[:conditions]
+ with_scope(:find => finder_options) do
+ ActiveSupport::Deprecation.silence { send(:#{finder}, options) }
+ end
+ else
+ ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) }
end
- else
- ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) }
- end
- end
- }, __FILE__, __LINE__
- send(method_id, *arguments)
- elsif match = matches_dynamic_finder_with_initialize_or_create?(method_id)
- instantiator = determine_instantiator(match)
- attribute_names = extract_attribute_names_from_match(match)
- super unless all_attributes_exists?(attribute_names)
-
- self.class_eval %{
- def self.#{method_id}(*args)
- guard_protected_attributes = false
-
- if args[0].is_a?(Hash)
- guard_protected_attributes = true
- attributes = args[0].with_indifferent_access
- find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
- else
- find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
+ #{'result || raise(RecordNotFound)' if bang}
end
+ }, __FILE__, __LINE__
+ send(method_id, *arguments)
+ elsif match.instantiator?
+ instantiator = match.instantiator
+ self.class_eval %{
+ def self.#{method_id}(*args)
+ guard_protected_attributes = false
+
+ if args[0].is_a?(Hash)
+ guard_protected_attributes = true
+ attributes = args[0].with_indifferent_access
+ find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
+ else
+ find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
+ end
- options = { :conditions => find_attributes }
- set_readonly_option!(options)
+ options = { :conditions => find_attributes }
+ set_readonly_option!(options)
- record = find_initial(options)
+ record = find_initial(options)
- if record.nil?
- record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
- #{'yield(record) if block_given?'}
- #{'record.save' if instantiator == :create}
- record
- else
- record
+ if record.nil?
+ record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
+ #{'yield(record) if block_given?'}
+ #{'record.save' if instantiator == :create}
+ record
+ else
+ record
+ end
end
- end
- }, __FILE__, __LINE__
- send(method_id, *arguments)
+ }, __FILE__, __LINE__
+ send(method_id, *arguments)
+ end
else
super
end
end
- def matches_dynamic_finder?(method_id)
- /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s)
- end
-
- def matches_dynamic_finder_with_initialize_or_create?(method_id)
- /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(method_id.to_s)
- end
-
- def determine_finder(match)
- match.captures.first == 'all_by' ? :find_every : :find_initial
- end
-
- def determine_instantiator(match)
- match.captures.first == 'initialize' ? :new : :create
- end
-
- def extract_attribute_names_from_match(match)
- match.captures.last.split('_and_')
- end
-
def construct_attributes_from_arguments(attribute_names, arguments)
attributes = {}
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
@@ -1717,7 +1764,7 @@ module ActiveRecord #:nodoc:
def attribute_condition(argument)
case argument
when nil then "IS ?"
- when Array, ActiveRecord::Associations::AssociationCollection then "IN (?)"
+ when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "IN (?)"
when Range then "BETWEEN ? AND ?"
else "= ?"
end
@@ -2372,7 +2419,11 @@ module ActiveRecord #:nodoc:
attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
attributes.each do |k, v|
- k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
+ if k.include?("(")
+ multi_parameter_attributes << [ k, v ]
+ else
+ respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
+ end
end
assign_multiparameter_attributes(multi_parameter_attributes)
@@ -2535,11 +2586,14 @@ module ActiveRecord #:nodoc:
end
def convert_number_column_value(value)
- case value
- when FalseClass; 0
- when TrueClass; 1
- when ''; nil
- else value
+ if value == false
+ 0
+ elsif value == true
+ 1
+ elsif value.is_a?(String) && value.blank?
+ nil
+ else
+ value
end
end
@@ -2558,7 +2612,7 @@ module ActiveRecord #:nodoc:
removed_attributes = attributes.keys - safe_attributes.keys
if removed_attributes.any?
- logger.debug "WARNING: Can't mass-assign these protected attributes: #{removed_attributes.join(', ')}"
+ log_protected_attribute_removal(removed_attributes)
end
safe_attributes
@@ -2573,6 +2627,10 @@ module ActiveRecord #:nodoc:
end
end
+ def log_protected_attribute_removal(*attributes)
+ logger.debug "WARNING: Can't mass-assign these protected attributes: #{attributes.join(', ')}"
+ end
+
# The primary key and inheritance column can never be set by mass-assignment for security reasons.
def attributes_protected_by_default
default = [ self.class.primary_key, self.class.inheritance_column ]
@@ -2586,8 +2644,15 @@ module ActiveRecord #:nodoc:
quoted = {}
connection = self.class.connection
attribute_names.each do |name|
- if column = column_for_attribute(name)
- quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && column.primary
+ if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
+ value = read_attribute(name)
+
+ # We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML.
+ if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))
+ value = value.to_yaml
+ end
+
+ quoted[name] = connection.quote(value, column)
end
end
include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb
index 2ca1a0aaa3..246f87b7a9 100644
--- a/activerecord/lib/active_record/calculations.rb
+++ b/activerecord/lib/active_record/calculations.rb
@@ -211,7 +211,7 @@ module ActiveRecord
sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options, scope)
- sql << ')' if use_workaround
+ sql << ') AS #{aggregate_alias}_subquery' if use_workaround
sql
end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 1e385fb128..eec531c514 100755..100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -50,7 +50,7 @@ module ActiveRecord
#
# == Inheritable callback queues
#
- # Besides the overwriteable callback methods, it's also possible to register callbacks through the use of the callback macros.
+ # Besides the overwritable callback methods, it's also possible to register callbacks through the use of the callback macros.
# Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance
# hierarchy. Example:
#
@@ -169,6 +169,18 @@ module ActiveRecord
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns
# +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
# defined as methods on the model, which are called last.
+ #
+ # == Transactions
+ #
+ # The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
+ # within a transaction. That includes <tt>after_*</tt> hooks. If everything
+ # goes fine a COMMIT is executed once the chain has been completed.
+ #
+ # If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
+ # can also trigger a ROLLBACK raising an exception in any of the callbacks,
+ # including <tt>after_*</tt> hooks. Note, however, that in that case the client
+ # needs to be aware of it because an ordinary +save+ will raise such exception
+ # instead of quietly returning +false+.
module Callbacks
CALLBACKS = %w(
after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 5358491cde..aaf9e2e73f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -149,6 +149,10 @@ module ActiveRecord
"INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)"
end
+ def case_sensitive_equality_operator
+ "="
+ end
+
protected
# Returns an array of record hashes with the column names as keys and
# column values as values.
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 31d6c7942c..75032efe57 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -138,7 +138,11 @@ module ActiveRecord
# convert something to a boolean
def value_to_boolean(value)
- TRUE_VALUES.include?(value)
+ if value.is_a?(String) && value.blank?
+ nil
+ else
+ TRUE_VALUES.include?(value)
+ end
end
# convert something to a BigDecimal
@@ -443,9 +447,10 @@ module ActiveRecord
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
# <tt>:updated_at</tt> to the table.
- def timestamps
- column(:created_at, :datetime)
- column(:updated_at, :datetime)
+ def timestamps(*args)
+ options = args.extract_options!
+ column(:created_at, :datetime, options)
+ column(:updated_at, :datetime, options)
end
def references(*args)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 7d8530ebef..bececf82a0 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -383,13 +383,9 @@ module ActiveRecord
def add_column_options!(sql, options) #:nodoc:
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
- # must explcitly check for :null to allow change_column to work on migrations
- if options.has_key? :null
- if options[:null] == false
- sql << " NOT NULL"
- else
- sql << " NULL"
- end
+ # must explicitly check for :null to allow change_column to work on migrations
+ if options[:null] == false
+ sql << " NOT NULL"
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index f48b107a2a..6924bb7e6f 100755..100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -51,6 +51,13 @@ module ActiveRecord
true
end
+ # Does this adapter support DDL rollbacks in transactions? That is, would
+ # CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
+ # SQL Server, and others support this. MySQL and others do not.
+ def supports_ddl_transactions?
+ false
+ end
+
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
# is called before each insert to set the record's primary key.
@@ -118,6 +125,19 @@ module ActiveRecord
@connection
end
+ def open_transactions
+ @open_transactions ||= 0
+ end
+
+ def increment_open_transactions
+ @open_transactions ||= 0
+ @open_transactions += 1
+ end
+
+ def decrement_open_transactions
+ @open_transactions -= 1
+ end
+
def log_info(sql, name, runtime)
if @logger && @logger.debug?
name = "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})"
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 35b9ed4746..204ebaa2e2 100755..100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -511,6 +511,10 @@ module ActiveRecord
keys.length == 1 ? [keys.first, nil] : nil
end
+ def case_sensitive_equality_operator
+ "= BINARY"
+ end
+
private
def connect
@connection.reconnect = true if @connection.respond_to?(:reconnect=)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 6d16d72dea..55c7da5b4f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -23,8 +23,8 @@ module ActiveRecord
config = config.symbolize_keys
host = config[:host]
port = config[:port] || 5432
- username = config[:username].to_s
- password = config[:password].to_s
+ username = config[:username].to_s if config[:username]
+ password = config[:password].to_s if config[:password]
if config.has_key?(:database)
database = config[:database]
@@ -182,8 +182,8 @@ module ActiveRecord
def self.extract_value_from_default(default)
case default
# Numeric types
- when /\A-?\d+(\.\d*)?\z/
- default
+ when /\A\(?(-?\d+(\.\d*)?\)?)\z/
+ $1
# Character types
when /\A'(.*)'::(?:character varying|bpchar|text)\z/m
$1
@@ -335,6 +335,10 @@ module ActiveRecord
postgresql_version >= 80200
end
+ def supports_ddl_transactions?
+ true
+ end
+
# Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x.
def table_alias_length
@@ -534,13 +538,13 @@ module ActiveRecord
option_string = options.symbolize_keys.sum do |key, value|
case key
when :owner
- " OWNER = '#{value}'"
+ " OWNER = \"#{value}\""
when :template
- " TEMPLATE = #{value}"
+ " TEMPLATE = \"#{value}\""
when :encoding
" ENCODING = '#{value}'"
when :tablespace
- " TABLESPACE = #{value}"
+ " TABLESPACE = \"#{value}\""
when :connection_limit
" CONNECTION LIMIT = #{value}"
else
@@ -761,7 +765,8 @@ module ActiveRecord
begin
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- rescue ActiveRecord::StatementInvalid
+ rescue ActiveRecord::StatementInvalid => e
+ raise e if postgresql_version > 80000
# This is PostgreSQL 7.x, so we have to use a more arcane way of doing it.
begin
begin_db_transaction
@@ -867,7 +872,7 @@ module ActiveRecord
end
private
- # The internal PostgreSQL identifer of the money data type.
+ # The internal PostgreSQL identifier of the money data type.
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
# Connects to a PostgreSQL server and sets up the adapter depending on the
diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb
index a7d767486c..63bf8c8f5b 100644
--- a/activerecord/lib/active_record/dirty.rb
+++ b/activerecord/lib/active_record/dirty.rb
@@ -62,7 +62,7 @@ module ActiveRecord
changed_attributes.keys
end
- # Map of changed attrs => [original value, new value]
+ # Map of changed attrs => [original value, new value].
# person.changes # => {}
# person.name = 'bob'
# person.changes # => { 'name' => ['bill', 'bob'] }
@@ -93,27 +93,27 @@ module ActiveRecord
end
private
- # Map of change attr => original value.
+ # Map of change <tt>attr => original value</tt>.
def changed_attributes
@changed_attributes ||= {}
end
- # Handle *_changed? for method_missing.
+ # Handle <tt>*_changed?</tt> for +method_missing+.
def attribute_changed?(attr)
changed_attributes.include?(attr)
end
- # Handle *_change for method_missing.
+ # Handle <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
end
- # Handle *_was for method_missing.
+ # Handle <tt>*_was</tt> for +method_missing+.
def attribute_was(attr)
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end
- # Handle *_will_change! for method_missing.
+ # Handle <tt>*_will_change!</tt> for +method_missing+.
def attribute_will_change!(attr)
changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
end
@@ -134,7 +134,9 @@ module ActiveRecord
def update_with_dirty
if partial_updates?
- update_without_dirty(changed)
+ # Serialized attributes should always be written in case they've been
+ # changed in place.
+ update_without_dirty(changed | self.class.serialized_attributes.keys)
else
update_without_dirty
end
diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb
new file mode 100644
index 0000000000..b105b919f5
--- /dev/null
+++ b/activerecord/lib/active_record/dynamic_finder_match.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ class DynamicFinderMatch
+ def self.match(method)
+ df_match = self.new(method)
+ df_match.finder ? df_match : nil
+ end
+
+ def initialize(method)
+ @finder = :find_initial
+ case method.to_s
+ when /^find_(all_by|by)_([_a-zA-Z]\w*)$/
+ @finder = :find_every if $1 == 'all_by'
+ names = $2
+ when /^find_by_([_a-zA-Z]\w*)\!$/
+ @bang = true
+ names = $1
+ when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
+ @instantiator = $1 == 'initialize' ? :new : :create
+ names = $2
+ else
+ @finder = nil
+ end
+ @attribute_names = names && names.split('_and_')
+ end
+
+ attr_reader :finder, :attribute_names, :instantiator
+
+ def finder?
+ !@finder.nil? && @instantiator.nil?
+ end
+
+ def instantiator?
+ @finder == :find_initial && !@instantiator.nil?
+ end
+
+ def bang?
+ @bang
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 17fb9355c4..622cfc3c3f 100755..100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -515,7 +515,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
all_loaded_fixtures.update(fixtures_map)
- connection.transaction(Thread.current['open_transactions'].to_i == 0) do
+ connection.transaction(connection.open_transactions.zero?) do
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
fixtures.each { |fixture| fixture.insert_fixtures }
@@ -930,7 +930,7 @@ module Test #:nodoc:
load_fixtures
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
- ActiveRecord::Base.send :increment_open_transactions
+ ActiveRecord::Base.connection.increment_open_transactions
ActiveRecord::Base.connection.begin_db_transaction
# Load fixtures for every test.
else
@@ -951,9 +951,9 @@ module Test #:nodoc:
end
# Rollback changes if a transaction is active.
- if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
+ if use_transactional_fixtures? && ActiveRecord::Base.connection.open_transactions != 0
ActiveRecord::Base.connection.rollback_db_transaction
- Thread.current['open_transactions'] = 0
+ ActiveRecord::Base.connection.decrement_open_transactions
end
ActiveRecord::Base.verify_active_connections!
end
diff --git a/activerecord/lib/active_record/locale/en-US.yml b/activerecord/lib/active_record/locale/en-US.yml
new file mode 100644
index 0000000000..8148f31a81
--- /dev/null
+++ b/activerecord/lib/active_record/locale/en-US.yml
@@ -0,0 +1,33 @@
+en-US:
+ activerecord:
+ errors:
+ # The values :model, :attribute and :value are always available for interpolation
+ # The value :count is available when applicable. Can be used for pluralization.
+ messages:
+ inclusion: "is not included in the list"
+ exclusion: "is reserved"
+ invalid: "is invalid"
+ confirmation: "doesn't match confirmation"
+ accepted: "must be accepted"
+ empty: "can't be empty"
+ blank: "can't be blank"
+ too_long: "is too long (maximum is {{count}} characters)"
+ too_short: "is too short (minimum is {{count}} characters)"
+ wrong_length: "is the wrong length (should be {{count}} characters)"
+ taken: "has already been taken"
+ not_a_number: "is not a number"
+ greater_than: "must be greater than {{count}}"
+ greater_than_or_equal_to: "must be greater than or equal to {{count}}"
+ equal_to: "must be equal to {{count}}"
+ less_than: "must be less than {{count}}"
+ less_than_or_equal_to: "must be less than or equal to {{count}}"
+ odd: "must be odd"
+ even: "must be even"
+ # Append your own errors here or at the model/attributes scope.
+
+ models:
+ # Overrides default messages
+
+ attributes:
+ # Overrides model and default messages.
+
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index e095b3c766..1d843fff28 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -238,6 +238,22 @@ module ActiveRecord
# lower than the current schema version: when migrating up, those
# never-applied "interleaved" migrations will be automatically applied, and
# when migrating down, never-applied "interleaved" migrations will be skipped.
+ #
+ # == Timestamped Migrations
+ #
+ # By default, Rails generates migrations that look like:
+ #
+ # 20080717013526_your_migration_name.rb
+ #
+ # The prefix is a generation timestamp (in UTC).
+ #
+ # If you'd prefer to use numeric prefixes, you can turn timestamped migrations
+ # off by setting:
+ #
+ # config.active_record.timestamped_migrations = false
+ #
+ # In environment.rb.
+ #
class Migration
@@verbose = true
cattr_accessor :verbose
@@ -333,6 +349,27 @@ module ActiveRecord
end
end
+ # MigrationProxy is used to defer loading of the actual migration classes
+ # until they are needed
+ class MigrationProxy
+
+ attr_accessor :name, :version, :filename
+
+ delegate :migrate, :announce, :write, :to=>:migration
+
+ private
+
+ def migration
+ @migration ||= load_migration
+ end
+
+ def load_migration
+ load(filename)
+ name.constantize
+ end
+
+ end
+
class Migrator#:nodoc:
class << self
def migrate(migrations_path, target_version = nil)
@@ -369,11 +406,17 @@ module ActiveRecord
Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
end
+ def get_all_versions
+ Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).sort
+ end
+
def current_version
- version = Base.connection.select_values(
- "SELECT version FROM #{schema_migrations_table_name}"
- ).map(&:to_i).max rescue nil
- version || 0
+ sm_table = schema_migrations_table_name
+ if Base.connection.table_exists?(sm_table)
+ get_all_versions.max || 0
+ else
+ 0
+ end
end
def proper_table_name(name)
@@ -389,7 +432,7 @@ module ActiveRecord
end
def current_version
- self.class.current_version
+ migrated.last || 0
end
def current_migration
@@ -421,17 +464,25 @@ module ActiveRecord
runnable.pop if down? && !target.nil?
runnable.each do |migration|
- Base.logger.info "Migrating to #{migration} (#{migration.version})"
+ Base.logger.info "Migrating to #{migration.name} (#{migration.version})"
# On our way up, we skip migrating the ones we've already migrated
- # On our way down, we skip reverting the ones we've never migrated
next if up? && migrated.include?(migration.version.to_i)
+ # On our way down, we skip reverting the ones we've never migrated
if down? && !migrated.include?(migration.version.to_i)
migration.announce 'never migrated, skipping'; migration.write
- else
- migration.migrate(@direction)
- record_version_state_after_migrating(migration.version)
+ next
+ end
+
+ begin
+ ddl_transaction do
+ migration.migrate(@direction)
+ record_version_state_after_migrating(migration.version)
+ end
+ rescue => e
+ canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
+ raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
end
end
end
@@ -454,11 +505,10 @@ module ActiveRecord
raise DuplicateMigrationNameError.new(name.camelize)
end
- load(file)
-
- klasses << returning(name.camelize.constantize) do |klass|
- class << klass; attr_accessor :version end
- klass.version = version
+ klasses << returning(MigrationProxy.new) do |migration|
+ migration.name = name.camelize
+ migration.version = version
+ migration.filename = file
end
end
@@ -473,17 +523,19 @@ module ActiveRecord
end
def migrated
- sm_table = self.class.schema_migrations_table_name
- Base.connection.select_values("SELECT version FROM #{sm_table}").map(&:to_i).sort
+ @migrated_versions ||= self.class.get_all_versions
end
private
def record_version_state_after_migrating(version)
sm_table = self.class.schema_migrations_table_name
+ @migrated_versions ||= []
if down?
+ @migrated_versions.delete(version.to_i)
Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'")
else
+ @migrated_versions.push(version.to_i).sort!
Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')")
end
end
@@ -495,5 +547,14 @@ module ActiveRecord
def down?
@direction == :down
end
+
+ # Wrap the migration in a transaction only if supported by the adapter.
+ def ddl_transaction(&block)
+ if Base.connection.supports_ddl_transactions?
+ Base.transaction { block.call }
+ else
+ block.call
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index 080e3d0f5e..c99c4beca9 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -103,7 +103,7 @@ module ActiveRecord
attr_reader :proxy_scope, :proxy_options
[].methods.each do |m|
- unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?)/
+ unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty?|any?|respond_to?)/
delegate m, :to => :proxy_found
end
end
@@ -140,6 +140,18 @@ module ActiveRecord
@found ? @found.empty? : count.zero?
end
+ def respond_to?(method, include_private = false)
+ super || @proxy_scope.respond_to?(method, include_private)
+ end
+
+ def any?
+ if block_given?
+ proxy_found.any? { |*block_args| yield(*block_args) }
+ else
+ !empty?
+ end
+ end
+
protected
def proxy_found
@found || load_found
diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb
index 25e0e61c69..b35e407cc1 100644
--- a/activerecord/lib/active_record/observer.rb
+++ b/activerecord/lib/active_record/observer.rb
@@ -20,7 +20,7 @@ module ActiveRecord
# ActiveRecord::Base.observers = Cacher, GarbageCollector
#
# Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is
- # called during startup, and before each development request.
+ # called during startup, and before each development request.
def observers=(*observers)
@observers = observers.flatten
end
@@ -130,11 +130,11 @@ module ActiveRecord
# Observers register themselves in the model class they observe, since it is the class that
# notifies them of events when they occur. As a side-effect, when an observer is loaded its
# corresponding model class is loaded.
- #
+ #
# Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
- # application initializers. Now observers are loaded after application initializers,
+ # application initializers. Now observers are loaded after application initializers,
# so observed models can make use of extensions.
- #
+ #
# If by any chance you are using observed models in the initialization you can still
# load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
# singletons and that call instantiates and registers them.
@@ -189,7 +189,9 @@ module ActiveRecord
def add_observer!(klass)
klass.add_observer(self)
- klass.class_eval 'def after_find() end' unless klass.method_defined?(:after_find)
+ if respond_to?(:after_find) && !klass.method_defined?(:after_find)
+ klass.class_eval 'def after_find() end'
+ end
end
end
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 3f74c03714..935b1939d8 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -109,7 +109,7 @@ module ActiveRecord
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
# and +other_aggregation+ has an options hash assigned to it.
def ==(other_aggregation)
- name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
+ other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
end
def sanitized_conditions #:nodoc:
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index ca5591ae35..ffaa41282f 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -37,7 +37,7 @@ module ActiveRecord
$queries_executed = []
yield
ensure
- assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed."
+ assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
end
def assert_no_queries(&block)
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 3b6835762c..81462a2917 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -66,32 +66,24 @@ module ActiveRecord
# will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction
# depends on or you can raise exceptions in the callbacks to rollback.
#
- # == Exception handling
+ # == Exception handling and rolling back
#
# Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you
- # should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will
- # trigger a ROLLBACK when raised, but not be re-raised by the transaction block.
+ # should be ready to catch those in your application code.
+ #
+ # One exception is the ActiveRecord::Rollback exception, which will trigger a ROLLBACK when raised,
+ # but not be re-raised by the transaction block.
module ClassMethods
+ # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(&block)
- increment_open_transactions
+ connection.increment_open_transactions
begin
- connection.transaction(Thread.current['start_db_transaction'], &block)
+ connection.transaction(connection.open_transactions == 1, &block)
ensure
- decrement_open_transactions
+ connection.decrement_open_transactions
end
end
-
- private
- def increment_open_transactions #:nodoc:
- open = Thread.current['open_transactions'] ||= 0
- Thread.current['start_db_transaction'] = open.zero?
- Thread.current['open_transactions'] = open + 1
- end
-
- def decrement_open_transactions #:nodoc:
- Thread.current['open_transactions'] -= 1
- end
end
def transaction(&block)
@@ -99,11 +91,11 @@ module ActiveRecord
end
def destroy_with_transactions #:nodoc:
- transaction { destroy_without_transactions }
+ with_transaction_returning_status(:destroy_without_transactions)
end
def save_with_transactions(perform_validation = true) #:nodoc:
- rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } }
+ rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
end
def save_with_transactions! #:nodoc:
@@ -126,5 +118,17 @@ module ActiveRecord
end
raise
end
+
+ # Executes +method+ within a transaction and captures its return value as a
+ # status flag. If the status is true the transaction is committed, otherwise
+ # a ROLLBACK is issued. In any case the status flag is returned.
+ def with_transaction_returning_status(method, *args)
+ status = nil
+ transaction do
+ status = send(method, *args)
+ raise ActiveRecord::Rollback unless status
+ end
+ status
+ end
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 1035308aa5..8fe4336bbc 100755..100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # Raised by save! and create! when the record is invalid. Use the
+ # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
# +record+ method to retrieve the record which did not validate.
# begin
# complex_operation_that_calls_save!_internally
@@ -18,69 +18,95 @@ module ActiveRecord
# determine whether the object is in a valid state to be saved. See usage example in Validations.
class Errors
include Enumerable
+
+ class << self
+ def default_error_messages
+ ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages').")
+ I18n.translate 'activerecord.errors.messages'
+ end
+ end
def initialize(base) # :nodoc:
@base, @errors = base, {}
end
- @@default_error_messages = {
- :inclusion => "is not included in the list",
- :exclusion => "is reserved",
- :invalid => "is invalid",
- :confirmation => "doesn't match confirmation",
- :accepted => "must be accepted",
- :empty => "can't be empty",
- :blank => "can't be blank",
- :too_long => "is too long (maximum is %d characters)",
- :too_short => "is too short (minimum is %d characters)",
- :wrong_length => "is the wrong length (should be %d characters)",
- :taken => "has already been taken",
- :not_a_number => "is not a number",
- :greater_than => "must be greater than %d",
- :greater_than_or_equal_to => "must be greater than or equal to %d",
- :equal_to => "must be equal to %d",
- :less_than => "must be less than %d",
- :less_than_or_equal_to => "must be less than or equal to %d",
- :odd => "must be odd",
- :even => "must be even"
- }
-
- # Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
- cattr_accessor :default_error_messages
-
-
# Adds an error to the base object instead of any particular attribute. This is used
# to report errors that don't tie to any specific attribute, but rather to the object
# as a whole. These error messages don't get prepended with any field name when iterating
- # with each_full, so they should be complete sentences.
+ # with +each_full+, so they should be complete sentences.
def add_to_base(msg)
add(:base, msg)
end
- # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
+ # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
# for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
- # If no +msg+ is supplied, "invalid" is assumed.
- def add(attribute, msg = @@default_error_messages[:invalid])
- @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
- @errors[attribute.to_s] << msg
+ # If no +messsage+ is supplied, :invalid is assumed.
+ # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
+ def add(attribute, message = nil, options = {})
+ message ||= :invalid
+ message = generate_message(attribute, message, options) if message.is_a?(Symbol)
+ @errors[attribute.to_s] ||= []
+ @errors[attribute.to_s] << message
end
# Will add an error message to each of the attributes in +attributes+ that is empty.
- def add_on_empty(attributes, msg = @@default_error_messages[:empty])
+ def add_on_empty(attributes, custom_message = nil)
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
- is_empty = value.respond_to?("empty?") ? value.empty? : false
- add(attr, msg) unless !value.nil? && !is_empty
+ is_empty = value.respond_to?("empty?") ? value.empty? : false
+ add(attr, :empty, :default => custom_message) unless !value.nil? && !is_empty
end
end
# Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
- def add_on_blank(attributes, msg = @@default_error_messages[:blank])
+ def add_on_blank(attributes, custom_message = nil)
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
- add(attr, msg) if value.blank?
+ add(attr, :blank, :default => custom_message) if value.blank?
+ end
+ end
+
+ # Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>).
+ # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there,
+ # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the
+ # default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name,
+ # translated attribute name and the value are available for interpolation.
+ #
+ # When using inheritence in your models, it will check all the inherited models too, but only if the model itself
+ # hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
+ # error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
+ #
+ # <ol>
+ # <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li>
+ # <li><tt>activerecord.errors.models.admin.blank</tt></li>
+ # <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li>
+ # <li><tt>activerecord.errors.models.user.blank</tt></li>
+ # <li><tt>activerecord.errors.messages.blank</tt></li>
+ # <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li>
+ # </ol>
+ def generate_message(attribute, message = :invalid, options = {})
+
+ defaults = @base.class.self_and_descendents_from_active_record.map do |klass|
+ [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
+ :"models.#{klass.name.underscore}.#{message}" ]
end
+
+ defaults << options.delete(:default)
+ defaults = defaults.compact.flatten << :"messages.#{message}"
+
+ model_name = @base.class.name
+ key = defaults.shift
+ value = @base.respond_to?(attribute) ? @base.send(attribute) : nil
+
+ options = { :default => defaults,
+ :model => @base.class.human_name,
+ :attribute => @base.class.human_attribute_name(attribute.to_s),
+ :value => value,
+ :scope => [:activerecord, :errors]
+ }.merge(options)
+
+ I18n.translate(key, options)
end
# Returns true if the specified +attribute+ has errors associated with it.
@@ -97,7 +123,7 @@ module ActiveRecord
!@errors[attribute.to_s].nil?
end
- # Returns nil, if no errors are associated with the specified +attribute+.
+ # Returns +nil+, if no errors are associated with the specified +attribute+.
# Returns the error message, if one error is associated with the specified +attribute+.
# Returns an array of error messages, if more than one error is associated with the specified +attribute+.
#
@@ -118,7 +144,7 @@ module ActiveRecord
alias :[] :on
- # Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute).
+ # Returns errors assigned to the base object through +add_to_base+ according to the normal rules of <tt>on(attribute)</tt>.
def on_base
on(:base)
end
@@ -131,15 +157,15 @@ module ActiveRecord
# end
#
# company = Company.create(:address => '123 First St.')
- # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # =>
- # name - is too short (minimum is 5 characters)
- # name - can't be blank
- # address - can't be blank
+ # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" }
+ # # => name - is too short (minimum is 5 characters)
+ # # name - can't be blank
+ # # address - can't be blank
def each
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
end
- # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
+ # Yields each full error message added. So <tt>Person.errors.add("first_name", "can't be empty")</tt> will be returned
# through iteration as "First name can't be empty".
#
# class Company < ActiveRecord::Base
@@ -148,10 +174,10 @@ module ActiveRecord
# end
#
# company = Company.create(:address => '123 First St.')
- # company.errors.each_full{|msg| puts msg } # =>
- # Name is too short (minimum is 5 characters)
- # Name can't be blank
- # Address can't be blank
+ # company.errors.each_full{|msg| puts msg }
+ # # => Name is too short (minimum is 5 characters)
+ # # Name can't be blank
+ # # Address can't be blank
def each_full
full_messages.each { |msg| yield msg }
end
@@ -166,22 +192,24 @@ module ActiveRecord
# company = Company.create(:address => '123 First St.')
# company.errors.full_messages # =>
# ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
- def full_messages
+ def full_messages(options = {})
full_messages = []
@errors.each_key do |attr|
- @errors[attr].each do |msg|
- next if msg.nil?
+ @errors[attr].each do |message|
+ next unless message
if attr == "base"
- full_messages << msg
+ full_messages << message
else
- full_messages << @base.class.human_attribute_name(attr) + " " + msg
+ #key = :"activerecord.att.#{@base.class.name.underscore.to_sym}.#{attr}"
+ attr_name = @base.class.human_attribute_name(attr)
+ full_messages << attr_name + ' ' + message
end
end
end
full_messages
- end
+ end
# Returns true if no errors have been added.
def empty?
@@ -209,13 +237,13 @@ module ActiveRecord
# end
#
# company = Company.create(:address => '123 First St.')
- # company.errors.to_xml # =>
- # <?xml version="1.0" encoding="UTF-8"?>
- # <errors>
- # <error>Name is too short (minimum is 5 characters)</error>
- # <error>Name can't be blank</error>
- # <error>Address can't be blank</error>
- # </errors>
+ # company.errors.to_xml
+ # # => <?xml version="1.0" encoding="UTF-8"?>
+ # # <errors>
+ # # <error>Name is too short (minimum is 5 characters)</error>
+ # # <error>Name can't be blank</error>
+ # # <error>Address can't be blank</error>
+ # # </errors>
def to_xml(options={})
options[:root] ||= "errors"
options[:indent] ||= 2
@@ -226,6 +254,7 @@ module ActiveRecord
full_messages.each { |msg| e.error(msg) }
end
end
+
end
@@ -261,7 +290,7 @@ module ActiveRecord
# person.errors.on "phone_number" # => "has invalid format"
# person.errors.each_full { |msg| puts msg }
# # => "Last name can't be empty\n" +
- # "Phone number has invalid format"
+ # # "Phone number has invalid format"
#
# person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
# person.save # => true (and person is now saved in the database)
@@ -300,7 +329,7 @@ module ActiveRecord
: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 unwieldly and
+ # 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:
@@ -388,13 +417,15 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_confirmation_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
+ configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" }))
validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
+ unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
+ record.errors.add(attr_name, :confirmation, :default => configuration[:message])
+ end
end
end
@@ -422,7 +453,7 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_acceptance_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
+ configuration = { :on => :save, :allow_nil => true, :accept => "1" }
configuration.update(attr_names.extract_options!)
db_cols = begin
@@ -434,7 +465,9 @@ module ActiveRecord
attr_accessor(*names)
validates_each(attr_names,configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept]
+ unless value == configuration[:accept]
+ record.errors.add(attr_name, :accepted, :default => configuration[:message])
+ end
end
end
@@ -461,7 +494,7 @@ module ActiveRecord
# method, proc or string should return or evaluate to a true or false value.
#
def validates_presence_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
+ configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
# can't use validates_each here, because it cannot cope with nonexistent attributes,
@@ -509,10 +542,7 @@ module ActiveRecord
def validates_length_of(*attrs)
# Merge given options with defaults.
options = {
- :too_long => ActiveRecord::Errors.default_error_messages[:too_long],
- :too_short => ActiveRecord::Errors.default_error_messages[:too_short],
- :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length],
- :tokenizer => lambda {|value| value.split(//)}
+ :tokenizer => lambda {|value| value.split(//)}
}.merge(DEFAULT_VALIDATION_OPTIONS)
options.update(attrs.extract_options!.symbolize_keys)
@@ -535,15 +565,12 @@ module ActiveRecord
when :within, :in
raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
- too_short = options[:too_short] % option_value.begin
- too_long = options[:too_long] % option_value.end
-
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
if value.nil? or value.size < option_value.begin
- record.errors.add(attr, too_short)
+ record.errors.add(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
elsif value.size > option_value.end
- record.errors.add(attr, too_long)
+ record.errors.add(attr, :too_long, :default => options[:too_long], :count => option_value.end)
end
end
when :is, :minimum, :maximum
@@ -553,11 +580,13 @@ module ActiveRecord
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
- message = (options[:message] || options[message_options[option]]) % option_value
-
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
- record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
+ unless !value.nil? and value.size.method(validity_checks[option])[option_value]
+ key = message_options[option]
+ custom_message = options[:message] || options[key]
+ record.errors.add(attr, key, :default => custom_message, :count => option_value)
+ end
end
end
end
@@ -599,7 +628,7 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_uniqueness_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
+ configuration = { :case_sensitive => true }
configuration.update(attr_names.extract_options!)
validates_each(attr_names,configuration) do |record, attr_name, value|
@@ -617,13 +646,24 @@ module ActiveRecord
# class (which has a database table to query from).
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
- if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?)
- condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}"
+ is_text_column = finder_class.columns_hash[attr_name.to_s].text?
+
+ if value.nil?
+ comparison_operator = "IS ?"
+ elsif is_text_column
+ comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
+ value = value.to_s
+ else
+ comparison_operator = "= ?"
+ end
+
+ sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
+
+ if value.nil? || (configuration[:case_sensitive] || !is_text_column)
+ condition_sql = "#{sql_attribute} #{comparison_operator}"
condition_params = [value]
else
- # sqlite has case sensitive SELECT query, while MySQL/Postgresql don't.
- # Hence, this is needed only for sqlite.
- condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}"
+ condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
condition_params = [value.downcase]
end
@@ -640,26 +680,10 @@ module ActiveRecord
condition_params << record.send(:id)
end
- results = finder_class.with_exclusive_scope do
- connection.select_all(
- construct_finder_sql(
- :select => "#{connection.quote_column_name(attr_name)}",
- :from => "#{finder_class.quoted_table_name}",
- :conditions => [condition_sql, *condition_params]
- )
- )
- end
-
- unless results.length.zero?
- found = true
-
- # As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate
- # column in ruby when case sensitive option
- if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text?
- found = results.any? { |a| a[attr_name.to_s] == value }
+ finder_class.with_exclusive_scope do
+ if finder_class.exists?([condition_sql, *condition_params])
+ record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
end
-
- record.errors.add(attr_name, configuration[:message]) if found
end
end
end
@@ -689,13 +713,15 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_format_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
+ configuration = { :on => :save, :with => nil }
configuration.update(attr_names.extract_options!)
raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message] % value) unless value.to_s =~ configuration[:with]
+ unless value.to_s =~ configuration[:with]
+ record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
+ end
end
end
@@ -719,7 +745,7 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_inclusion_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
+ configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
enum = configuration[:in] || configuration[:within]
@@ -727,7 +753,9 @@ module ActiveRecord
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value)
+ unless enum.include?(value)
+ record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value)
+ end
end
end
@@ -751,7 +779,7 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_exclusion_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save }
+ configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
enum = configuration[:in] || configuration[:within]
@@ -759,7 +787,9 @@ module ActiveRecord
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value)
+ if enum.include?(value)
+ record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value)
+ end
end
end
@@ -795,12 +825,13 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_associated(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
+ configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message]) unless
- (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
+ unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
+ record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
+ end
end
end
@@ -848,15 +879,15 @@ module ActiveRecord
if configuration[:only_integer]
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
- record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
+ record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
raw_value = raw_value.to_i
else
- begin
+ begin
raw_value = Kernel.Float(raw_value)
rescue ArgumentError, TypeError
- record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
+ record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
end
@@ -864,11 +895,11 @@ module ActiveRecord
numericality_options.each do |option|
case option
when :odd, :even
- record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
+ unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
+ record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message])
+ end
else
- message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option]
- message = message % configuration[option] if configuration[option]
- record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
+ record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
end
end
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 9c718c4fef..9c718c4fef 100755..100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 3631be76a0..8c9ae8a031 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -9,7 +9,7 @@ require 'models/topic'
require 'models/reply'
class CascadedEagerLoadingTest < ActiveRecord::TestCase
- fixtures :authors, :mixins, :companies, :posts, :topics
+ fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, :categorizations
def test_eager_association_loading_with_cascaded_two_levels
authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
@@ -68,6 +68,18 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
end
+ def test_eager_association_loading_with_has_many_sti_and_subclasses
+ silly = SillyReply.new(:title => "gaga", :content => "boo-boo", :parent_id => 1)
+ silly.parent_id = 1
+ assert silly.save
+
+ topics = Topic.find(:all, :include => :replies, :order => 'topics.id, replies_topics.id')
+ assert_no_queries do
+ assert_equal 2, topics[0].replies.size
+ assert_equal 0, topics[1].replies.size
+ end
+ end
+
def test_eager_association_loading_with_belongs_to_sti
replies = Reply.find(:all, :include => :topic, :order => 'topics.id')
assert replies.include?(topics(:second))
diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
new file mode 100644
index 0000000000..7c470616a5
--- /dev/null
+++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
@@ -0,0 +1,36 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/tagging'
+
+module Namespaced
+ class Post < ActiveRecord::Base
+ set_table_name 'posts'
+ has_one :tagging, :as => :taggable, :class_name => 'Tagging'
+ end
+end
+
+class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase
+
+ def setup
+ generate_test_objects
+ end
+
+ def generate_test_objects
+ post = Namespaced::Post.create( :title => 'Great stuff', :body => 'This is not', :author_id => 1 )
+ tagging = Tagging.create( :taggable => post )
+ end
+
+ def test_class_names
+ old = ActiveRecord::Base.store_full_sti_class
+
+ ActiveRecord::Base.store_full_sti_class = false
+ post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging )
+ assert_nil post.tagging
+
+ ActiveRecord::Base.store_full_sti_class = true
+ post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging )
+ assert_equal 'Tagging', post.tagging.class.name
+ ensure
+ ActiveRecord::Base.store_full_sti_class = old
+ end
+end
diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
index 80cfc84b32..12dec5ccd1 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -1,5 +1,20 @@
require 'cases/helper'
+module Remembered
+ def self.included(base)
+ base.extend ClassMethods
+ base.class_eval do
+ after_create :remember
+ protected
+ def remember; self.class.remembered << self; end
+ end
+ end
+
+ module ClassMethods
+ def remembered; @@remembered ||= []; end
+ def rand; @@remembered.rand; end
+ end
+end
class ShapeExpression < ActiveRecord::Base
belongs_to :shape, :polymorphic => true
@@ -8,26 +23,33 @@ end
class Circle < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
+ include Remembered
end
class Square < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
+ include Remembered
end
class Triangle < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
+ include Remembered
end
class PaintColor < ActiveRecord::Base
has_many :shape_expressions, :as => :paint
belongs_to :non_poly, :foreign_key => "non_poly_one_id", :class_name => "NonPolyOne"
+ include Remembered
end
class PaintTexture < ActiveRecord::Base
has_many :shape_expressions, :as => :paint
belongs_to :non_poly, :foreign_key => "non_poly_two_id", :class_name => "NonPolyTwo"
+ include Remembered
end
class NonPolyOne < ActiveRecord::Base
has_many :paint_colors
+ include Remembered
end
class NonPolyTwo < ActiveRecord::Base
has_many :paint_textures
+ include Remembered
end
@@ -49,23 +71,19 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
end
- # meant to be supplied as an ID, never returns 0
- def rand_simple
- val = (NUM_SIMPLE_OBJS * rand).round
- val == 0 ? 1 : val
- end
-
def generate_test_object_graphs
1.upto(NUM_SIMPLE_OBJS) do
[Circle, Square, Triangle, NonPolyOne, NonPolyTwo].map(&:create!)
end
- 1.upto(NUM_SIMPLE_OBJS) do |i|
- PaintColor.create!(:non_poly_one_id => rand_simple)
- PaintTexture.create!(:non_poly_two_id => rand_simple)
+ 1.upto(NUM_SIMPLE_OBJS) do
+ PaintColor.create!(:non_poly_one_id => NonPolyOne.rand.id)
+ PaintTexture.create!(:non_poly_two_id => NonPolyTwo.rand.id)
end
- 1.upto(NUM_SHAPE_EXPRESSIONS) do |i|
- ShapeExpression.create!(:shape_type => [Circle, Square, Triangle].rand.to_s, :shape_id => rand_simple,
- :paint_type => [PaintColor, PaintTexture].rand.to_s, :paint_id => rand_simple)
+ 1.upto(NUM_SHAPE_EXPRESSIONS) do
+ shape_type = [Circle, Square, Triangle].rand
+ paint_type = [PaintColor, PaintTexture].rand
+ ShapeExpression.create!(:shape_type => shape_type.to_s, :shape_id => shape_type.rand.id,
+ :paint_type => paint_type.to_s, :paint_id => paint_type.rand.id)
end
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index f65ada550b..e78624a98d 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -21,7 +21,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
fixtures :posts, :comments, :authors, :categories, :categories_posts,
:companies, :accounts, :tags, :taggings, :people, :readers,
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
- :developers, :projects
+ :developers, :projects, :developers_projects
def test_loading_with_one_association
posts = Post.find(:all, :include => :comments)
@@ -38,6 +38,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal Post.find(1).last_comment, post.last_comment
end
+ def test_loading_with_one_association_with_non_preload
+ posts = Post.find(:all, :include => :last_comment, :order => 'comments.id DESC')
+ post = posts.find { |p| p.id == 1 }
+ assert_equal Post.find(1).last_comment, post.last_comment
+ end
+
def test_loading_conditions_with_or
posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'")
assert_nil posts.detect { |p| p.author_id != authors(:david).id },
@@ -254,9 +260,9 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_through
- posts_with_comments = people(:michael).posts.find(:all, :include => :comments)
- posts_with_author = people(:michael).posts.find(:all, :include => :author )
- posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ])
+ posts_with_comments = people(:michael).posts.find(:all, :include => :comments, :order => 'posts.id')
+ posts_with_author = people(:michael).posts.find(:all, :include => :author, :order => 'posts.id')
+ posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ], :order => 'posts.id')
assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size }
assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
@@ -559,6 +565,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_nothing_raised { Post.find(:all, :include => 'comments') }
end
+ def test_eager_with_floating_point_numbers
+ assert_queries(2) do
+ # Before changes, the floating point numbers will be interpreted as table names and will cause this to run in one query
+ Comment.find :all, :conditions => "123.456 = 123.456", :include => :post
+ end
+ end
+
def test_preconfigured_includes_with_belongs_to
author = posts(:welcome).author_with_posts
assert_no_queries {assert_equal 5, author.posts.size}
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index b29df68d22..0572418e3b 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -70,7 +70,7 @@ end
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
- :parrots, :pirates, :treasures, :price_estimates
+ :parrots, :pirates, :treasures, :price_estimates, :tags, :taggings
def test_has_and_belongs_to_many
david = Developer.find(1)
@@ -299,6 +299,17 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, projects(:active_record, :reload).developers.size
end
+ def test_uniq_option_prevents_duplicate_push
+ project = projects(:active_record)
+ project.developers << developers(:jamis)
+ project.developers << developers(:david)
+ assert_equal 3, project.developers.size
+
+ project.developers << developers(:david)
+ project.developers << developers(:jamis)
+ assert_equal 3, project.developers.size
+ end
+
def test_deleting
david = Developer.find(1)
active_record = Project.find(1)
@@ -439,6 +450,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find"
end
+ def test_find_in_association_with_custom_finder_sql_and_multiple_interpolations
+ # interpolate once:
+ assert_equal [developers(:david), developers(:jamis), developers(:poor_jamis)], projects(:active_record).developers_with_finder_sql, "first interpolation"
+ # interpolate again, for a different project id
+ assert_equal [developers(:david)], projects(:action_controller).developers_with_finder_sql, "second interpolation"
+ end
+
def test_find_in_association_with_custom_finder_sql_and_string_id
assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find"
end
@@ -629,8 +647,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer.save
developer.reload
assert_equal 2, developer.projects.length
- assert_equal projects(:active_record), developer.projects[0]
- assert_equal projects(:action_controller), developer.projects[1]
+ assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
end
def test_assign_ids_ignoring_blanks
@@ -639,8 +656,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer.save
developer.reload
assert_equal 2, developer.projects.length
- assert_equal projects(:active_record), developer.projects[0]
- assert_equal projects(:action_controller), developer.projects[1]
+ assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
end
def test_select_limited_ids_list
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index b9c7ec6377..da3c8fb28e 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -14,7 +14,7 @@ require 'models/reader'
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments, :author_addresses,
- :people, :posts
+ :people, :posts, :readers
def setup
Client.destroyed_client_ids.clear
@@ -395,6 +395,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, company.clients_of_firm.size
end
+ def test_collection_size_twice_for_regressions
+ post = posts(:thinking)
+ assert_equal 0, post.readers.size
+ # This test needs a post that has no readers, we assert it to ensure it holds,
+ # but need to reload the post because the very call to #size hides the bug.
+ post.reload
+ post.readers.build
+ size1 = post.readers.size
+ size2 = post.readers.size
+ assert_equal size1, size2
+ end
+
def test_build_many
company = companies(:first_firm)
new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
@@ -999,4 +1011,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert firm.clients.loaded?
end
+ def test_joins_with_namespaced_model_should_use_correct_type
+ old = ActiveRecord::Base.store_full_sti_class
+ ActiveRecord::Base.store_full_sti_class = true
+
+ firm = Namespaced::Firm.create({ :name => 'Some Company' })
+ firm.clients.create({ :name => 'Some Client' })
+
+ stats = Namespaced::Firm.find(firm.id, {
+ :select => "#{Namespaced::Firm.table_name}.id, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients",
+ :joins => :clients,
+ :group => "#{Namespaced::Firm.table_name}.id"
+ })
+ assert_equal 1, stats.num_clients.to_i
+
+ ensure
+ ActiveRecord::Base.store_full_sti_class = old
+ end
+
end
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index be5170f830..d51a3c7e1c 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -2,15 +2,18 @@ require "cases/helper"
require 'models/post'
require 'models/person'
require 'models/reader'
+require 'models/comment'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
- fixtures :posts, :readers, :people
+ fixtures :posts, :readers, :people, :comments
def test_associate_existing
assert_queries(2) { posts(:thinking);people(:david) }
-
+
+ posts(:thinking).people
+
assert_queries(1) do
- posts(:thinking).people << people(:david)
+ posts(:thinking).people << people(:david)
end
assert_queries(1) do
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 99639849a5..ec06be5eba 100755..100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -79,6 +79,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
end
+ def test_natural_assignment_to_already_associated_record
+ company = companies(:first_firm)
+ account = accounts(:signals37)
+ assert_equal company.account, account
+ company.account = account
+ company.reload
+ account.reload
+ assert_equal company.account, account
+ end
+
def test_assignment_without_replacement
apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10)
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index 3eb66bc941..b61a3711e3 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -44,19 +44,23 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_polymorphic
assert_equal clubs(:moustache_club), @member.sponsor_club
end
-
+
def has_one_through_to_has_many
assert_equal 2, @member.fellow_members.size
end
-
+
def test_has_one_through_eager_loading
- members = Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"])
+ members = assert_queries(3) do #base table, through table, clubs table
+ Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"])
+ end
assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].club}
end
-
+
def test_has_one_through_eager_loading_through_polymorphic
- members = Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"])
+ members = assert_queries(3) do #base table, through table, clubs table
+ Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"])
+ end
assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].sponsor_club}
end
@@ -71,4 +75,30 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_not_nil assert_no_queries {clubs[0].sponsored_member}
end
+ def test_has_one_through_nonpreload_eagerloading
+ members = assert_queries(1) do
+ Member.find(:all, :include => :club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries {members[0].club}
+ end
+
+ def test_has_one_through_nonpreload_eager_loading_through_polymorphic
+ members = assert_queries(1) do
+ Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries {members[0].sponsor_club}
+ end
+
+ def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record
+ Sponsor.new(:sponsor_club => clubs(:crazy_club), :sponsorable => members(:groucho)).save!
+ members = assert_queries(1) do
+ Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC') #force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries { members[0].sponsor_club }
+ assert_equal clubs(:crazy_club), members[0].sponsor_club
+ end
+
end
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 9e79d9c8a1..7a0427aabc 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -694,6 +694,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert ! david.categories.include?(category)
end
+ def test_has_many_through_goes_through_all_sti_classes
+ sub_sti_post = SubStiPost.create!(:title => 'test', :body => 'test', :author_id => 1)
+ new_comment = sub_sti_post.comments.create(:body => 'test')
+
+ assert_equal [9, 10, new_comment.id], authors(:david).sti_post_comments.map(&:id).sort
+ end
+
private
# create dynamic Post models to allow different dependency options
def find_post_with_dependency(post_id, association, association_name, dependency)
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index 4904feeb7d..0b2731ecd7 100755..100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -27,7 +27,7 @@ require 'models/sponsor'
class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
- :computers
+ :computers, :people, :readers
def test_include_with_order_works
assert_nothing_raised {Account.find(:first, :order => 'id', :include => :firm)}
@@ -45,7 +45,7 @@ class AssociationsTest < ActiveRecord::TestCase
assert_equal [], person.readers.find(:all)
person.save!
reader = Reader.create! :person => person, :post => Post.new(:title => "foo", :body => "bar")
- assert_equal [reader], person.readers.find(:all)
+ assert person.readers.find(reader.id)
end
def test_force_reload
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 7999e29264..7999e29264 100755..100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index a4be629fbd..c8111358e3 100755..100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -19,6 +19,7 @@ require 'models/warehouse_thing'
require 'rexml/document'
class Category < ActiveRecord::Base; end
+class Categorization < ActiveRecord::Base; end
class Smarts < ActiveRecord::Base; end
class CreditCard < ActiveRecord::Base
class PinNumber < ActiveRecord::Base
@@ -75,7 +76,7 @@ class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base
end
class BasicsTest < ActiveRecord::TestCase
- fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors
+ fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories
def test_table_exists
assert !NonExistentTable.table_exists?
@@ -130,14 +131,14 @@ class BasicsTest < ActiveRecord::TestCase
def test_read_attributes_before_type_cast
category = Category.new({:name=>"Test categoty", :type => nil})
- category_attrs = {"name"=>"Test categoty", "type" => nil}
+ category_attrs = {"name"=>"Test categoty", "type" => nil, "categorizations_count" => nil}
assert_equal category_attrs , category.attributes_before_type_cast
end
if current_adapter?(:MysqlAdapter)
def test_read_attributes_before_type_cast_on_boolean
bool = Booleantest.create({ "value" => false })
- assert_equal 0, bool.attributes_before_type_cast["value"]
+ assert_equal "0", bool.reload.attributes_before_type_cast["value"]
end
end
@@ -614,6 +615,22 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal -2, Topic.find(2).replies_count
end
+ def test_update_counter
+ category = categories(:general)
+ assert_nil category.categorizations_count
+ assert_equal 2, category.categorizations.count
+
+ Category.update_counters(category.id, "categorizations_count" => category.categorizations.count)
+ category.reload
+ assert_not_nil category.categorizations_count
+ assert_equal 2, category.categorizations_count
+
+ Category.update_counters(category.id, "categorizations_count" => category.categorizations.count)
+ category.reload
+ assert_not_nil category.categorizations_count
+ assert_equal 4, category.categorizations_count
+ end
+
def test_update_all
assert_equal Topic.count, Topic.update_all("content = 'bulk updated!'")
assert_equal "bulk updated!", Topic.find(1).content
@@ -887,6 +904,14 @@ class BasicsTest < ActiveRecord::TestCase
assert_nil keyboard.id
end
+ def test_mass_assigning_invalid_attribute
+ firm = Firm.new
+
+ assert_raises(ActiveRecord::UnknownAttributeError) do
+ firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 }
+ end
+ end
+
def test_mass_assignment_protection_on_defaults
firm = Firm.new
firm.attributes = { "id" => 5, "type" => "Client" }
@@ -1089,11 +1114,15 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_boolean
+ b_nil = Booleantest.create({ "value" => nil })
+ nil_id = b_nil.id
b_false = Booleantest.create({ "value" => false })
false_id = b_false.id
b_true = Booleantest.create({ "value" => true })
true_id = b_true.id
+ b_nil = Booleantest.find(nil_id)
+ assert_nil b_nil.value
b_false = Booleantest.find(false_id)
assert !b_false.value?
b_true = Booleantest.find(true_id)
@@ -1101,11 +1130,15 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_boolean_cast_from_string
+ b_blank = Booleantest.create({ "value" => "" })
+ blank_id = b_blank.id
b_false = Booleantest.create({ "value" => "0" })
false_id = b_false.id
b_true = Booleantest.create({ "value" => "1" })
true_id = b_true.id
+ b_blank = Booleantest.find(blank_id)
+ assert_nil b_blank.value
b_false = Booleantest.find(false_id)
assert !b_false.value?
b_true = Booleantest.find(true_id)
@@ -1336,6 +1369,12 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal(myobj, topic.content)
end
+ def test_serialized_time_attribute
+ myobj = Time.local(2008,1,1,1,0)
+ topic = Topic.create("content" => myobj).reload
+ assert_equal(myobj, topic.content)
+ end
+
def test_nil_serialized_attribute_with_class_constraint
myobj = MyObject.new('value1', 'value2')
topic = Topic.new
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index 540f42f4b6..98abc8eac8 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -9,13 +9,13 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
end
# Avoid column definitions in create table statements like:
- # `title` varchar(255) DEFAULT NULL NULL
+ # `title` varchar(255) DEFAULT NULL
def test_should_not_include_default_clause_when_default_is_null
column = ActiveRecord::ConnectionAdapters::Column.new("title", nil, "varchar(20)")
column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new(
@adapter, column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal "title varchar(20) NULL", column_def.to_sql
+ assert_equal "title varchar(20)", column_def.to_sql
end
def test_should_include_default_clause_when_default_is_present
@@ -23,7 +23,7 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new(
@adapter, column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal %Q{title varchar(20) DEFAULT 'Hello' NULL}, column_def.to_sql
+ assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, column_def.to_sql
end
def test_should_specify_not_null_if_null_option_is_false
@@ -33,4 +33,4 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
column.limit, column.precision, column.scale, column.default, column.null)
assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, column_def.to_sql
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 2ea85417da..3473b846a0 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -5,7 +5,7 @@ require 'models/entrant'
class DefaultTest < ActiveRecord::TestCase
def test_nil_defaults_for_not_null_columns
column_defaults =
- if current_adapter?(:MysqlAdapter) && Mysql.client_version < 50051
+ if current_adapter?(:MysqlAdapter) && (Mysql.client_version < 50051 || (50100..50122).include?(Mysql.client_version))
{ 'id' => nil, 'name' => '', 'course_id' => nil }
else
{ 'id' => nil, 'name' => nil, 'course_id' => nil }
diff --git a/activerecord/test/cases/deprecated_finder_test.rb b/activerecord/test/cases/deprecated_finder_test.rb
index 2afc91b769..2afc91b769 100755..100644
--- a/activerecord/test/cases/deprecated_finder_test.rb
+++ b/activerecord/test/cases/deprecated_finder_test.rb
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index e5e022050d..feb47a15a8 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -191,6 +191,18 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.changed?
end
+ def test_save_should_store_serialized_attributes_even_with_partial_updates
+ with_partial_updates(Topic) do
+ topic = Topic.create!(:content => {:a => "a"})
+ topic.content[:b] = "b"
+ #assert topic.changed? # Known bug, will fail
+ topic.save!
+ assert_equal "b", topic.content[:b]
+ topic.reload
+ assert_equal "b", topic.content[:b]
+ end
+ end
+
private
def with_partial_updates(klass, on = true)
old = klass.partial_updates?
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index b97db73b68..2ce49ed76f 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -12,6 +12,57 @@ require 'models/customer'
require 'models/job'
require 'models/categorization'
+class DynamicFinderMatchTest < ActiveRecord::TestCase
+ def test_find_no_match
+ assert_nil ActiveRecord::DynamicFinderMatch.match("not_a_finder")
+ end
+
+ def test_find_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert match.finder?
+ assert_equal :find_initial, match.finder
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def find_by_bang
+ match = ActiveRecord::DynamicFinderMatch.match("find_by_age_and_sex_and_location!")
+ assert_not_nil match
+ assert match.finder?
+ assert match.bang?
+ assert_equal :find_initial, match.finder
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def test_find_all_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_all_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert match.finder?
+ assert_equal :find_every, match.finder
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def test_find_or_initialize_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_or_initialize_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert !match.finder?
+ assert match.instantiator?
+ assert_equal :find_initial, match.finder
+ assert_equal :new, match.instantiator
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def test_find_or_create_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_or_create_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert !match.finder?
+ assert match.instantiator?
+ assert_equal :find_initial, match.finder
+ assert_equal :create, match.instantiator
+ assert_equal %w(age sex location), match.attribute_names
+ end
+end
+
class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers
@@ -440,6 +491,11 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Topic.find_by_title("The First Topic!")
end
+ def test_find_by_one_attribute_bang
+ assert_equal topics(:first), Topic.find_by_title!("The First Topic")
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
+ end
+
def test_find_by_one_attribute_caches_dynamic_finder
# ensure this test can run independently of order
class << Topic; self; end.send(:remove_method, :find_by_title) if Topic.public_methods.any? { |m| m.to_s == 'find_by_title' }
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 0ea24868f1..6ba7597f56 100755..100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -461,11 +461,11 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase
alias_method :teardown, :blank_teardown
def test_no_rollback_in_teardown_unless_transaction_active
- assert_equal 0, Thread.current['open_transactions']
+ assert_equal 0, ActiveRecord::Base.connection.open_transactions
assert_raise(RuntimeError) { ar_setup_fixtures }
- assert_equal 0, Thread.current['open_transactions']
+ assert_equal 0, ActiveRecord::Base.connection.open_transactions
assert_nothing_raised { ar_teardown_fixtures }
- assert_equal 0, Thread.current['open_transactions']
+ assert_equal 0, ActiveRecord::Base.connection.open_transactions
end
private
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 0530ba9bd9..f30d58546e 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -1,4 +1,5 @@
$:.unshift(File.dirname(__FILE__) + '/../../lib')
+$:.unshift(File.dirname(__FILE__) + '/../../../activesupport/lib')
require 'config'
require 'test/unit'
diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb
new file mode 100644
index 0000000000..06036733f5
--- /dev/null
+++ b/activerecord/test/cases/i18n_test.rb
@@ -0,0 +1,46 @@
+require "cases/helper"
+require 'models/topic'
+require 'models/reply'
+
+class ActiveRecordI18nTests < Test::Unit::TestCase
+
+ def setup
+ reset_translations
+ end
+
+ def test_translated_model_attributes
+ I18n.store_translations 'en-US', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
+ assert_equal 'topic title attribute', Topic.human_attribute_name('title')
+ end
+
+ def test_translated_model_attributes_with_sti
+ I18n.store_translations 'en-US', :activerecord => {:attributes => {:reply => {:title => 'reply title attribute'} } }
+ assert_equal 'reply title attribute', Reply.human_attribute_name('title')
+ end
+
+ def test_translated_model_attributes_with_sti_fallback
+ I18n.store_translations 'en-US', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
+ assert_equal 'topic title attribute', Reply.human_attribute_name('title')
+ end
+
+ def test_translated_model_names
+ I18n.store_translations 'en-US', :activerecord => {:models => {:topic => 'topic model'} }
+ assert_equal 'topic model', Topic.human_name
+ end
+
+ def test_translated_model_names_with_sti
+ I18n.store_translations 'en-US', :activerecord => {:models => {:reply => 'reply model'} }
+ assert_equal 'reply model', Reply.human_name
+ end
+
+ def test_translated_model_names_with_sti_fallback
+ I18n.store_translations 'en-US', :activerecord => {:models => {:topic => 'topic model'} }
+ assert_equal 'topic model', Reply.human_name
+ end
+
+ private
+ def reset_translations
+ I18n.backend = I18n::Backend::Simple.new
+ end
+end
+
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 4fd38bfbc9..4fd38bfbc9 100755..100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb
index 258f7c7a0f..54fb3d8c39 100755..100644
--- a/activerecord/test/cases/lifecycle_test.rb
+++ b/activerecord/test/cases/lifecycle_test.rb
@@ -74,7 +74,7 @@ class MultiObserver < ActiveRecord::Observer
end
class LifecycleTest < ActiveRecord::TestCase
- fixtures :topics, :developers
+ fixtures :topics, :developers, :minimalistics
def test_before_destroy
original_count = Topic.count
@@ -143,7 +143,7 @@ class LifecycleTest < ActiveRecord::TestCase
assert_equal developer.name, multi_observer.record.name
end
- def test_observing_after_find_when_not_defined_on_the_model
+ def test_after_find_can_be_observed_when_its_not_defined_on_the_model
observer = MinimalisticObserver.instance
assert_equal Minimalistic, MinimalisticObserver.observed_class
@@ -151,6 +151,42 @@ class LifecycleTest < ActiveRecord::TestCase
assert_equal minimalistic, observer.minimalistic
end
+ def test_after_find_can_be_observed_when_its_defined_on_the_model
+ observer = TopicObserver.instance
+ assert_equal Topic, TopicObserver.observed_class
+
+ topic = Topic.find(1)
+ assert_equal topic, observer.topic
+ end
+
+ def test_after_find_is_not_created_if_its_not_used
+ # use a fresh class so an observer can't have defined an
+ # after_find on it
+ model_class = Class.new(ActiveRecord::Base)
+ observer_class = Class.new(ActiveRecord::Observer)
+ observer_class.observe(model_class)
+
+ observer = observer_class.instance
+
+ assert !model_class.method_defined?(:after_find)
+ end
+
+ def test_after_find_is_not_clobbered_if_it_already_exists
+ # use a fresh observer class so we can instantiate it (Observer is
+ # a Singleton)
+ model_class = Class.new(ActiveRecord::Base) do
+ def after_find; end
+ end
+ original_method = model_class.instance_method(:after_find)
+ observer_class = Class.new(ActiveRecord::Observer) do
+ def after_find; end
+ end
+ observer_class.observe(model_class)
+
+ observer = observer_class.instance
+ assert_equal original_method, model_class.instance_method(:after_find)
+ end
+
def test_invalid_observer
assert_raise(ArgumentError) { Topic.observers = Object.new; Topic.instantiate_observers }
end
diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb
index d6b3e341df..ee66ac948d 100644
--- a/activerecord/test/cases/method_scoping_test.rb
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -6,7 +6,7 @@ require 'models/post'
require 'models/category'
class MethodScopingTest < ActiveRecord::TestCase
- fixtures :developers, :projects, :comments, :posts
+ fixtures :developers, :projects, :comments, :posts, :developers_projects
def test_set_conditions
Developer.with_scope(:find => { :conditions => 'just a test...' }) do
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 7ecf755ef8..c1a8da2270 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -237,6 +237,39 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
+ def test_create_table_with_timestamps_should_create_datetime_columns
+ table_name = :testings
+
+ Person.connection.create_table table_name do |t|
+ t.timestamps
+ end
+ created_columns = Person.connection.columns(table_name)
+
+ created_at_column = created_columns.detect {|c| c.name == 'created_at' }
+ updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+
+ assert created_at_column.null
+ assert updated_at_column.null
+ ensure
+ Person.connection.drop_table table_name rescue nil
+ end
+
+ def test_create_table_with_timestamps_should_create_datetime_columns_with_options
+ table_name = :testings
+
+ Person.connection.create_table table_name do |t|
+ t.timestamps :null => false
+ end
+ created_columns = Person.connection.columns(table_name)
+
+ created_at_column = created_columns.detect {|c| c.name == 'created_at' }
+ updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+
+ assert !created_at_column.null
+ assert !updated_at_column.null
+ ensure
+ Person.connection.drop_table table_name rescue nil
+ end
# SQL Server, Sybase, and SQLite3 will not allow you to add a NOT NULL
# column to a table without a default value.
@@ -409,10 +442,7 @@ if ActiveRecord::Base.connection.supports_migrations?
ActiveRecord::Migration.add_column :people, :intelligence_quotient, :tinyint
Person.reset_column_information
- Person.create :intelligence_quotient => 300
- jonnyg = Person.find(:first)
- assert_equal 127, jonnyg.intelligence_quotient
- jonnyg.destroy
+ assert_match /tinyint/, Person.columns_hash['intelligence_quotient'].sql_type
ensure
ActiveRecord::Migration.remove_column :people, :intelligence_quotient rescue nil
end
@@ -904,6 +934,21 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal(0, ActiveRecord::Migrator.current_version)
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_migrator_one_up_with_exception_and_rollback
+ assert !Person.column_methods_hash.include?(:last_name)
+
+ e = assert_raises(StandardError) do
+ ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/broken", 100)
+ end
+
+ assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
+
+ Person.reset_column_information
+ assert !Person.column_methods_hash.include?(:last_name)
+ end
+ end
+
def test_finds_migrations
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations
[['1', 'people_have_last_names'],
@@ -922,6 +967,26 @@ if ActiveRecord::Base.connection.supports_migrations?
migrations[0].name == 'innocent_jointable'
end
+ def test_only_loads_pending_migrations
+ # migrate up to 1
+ ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1)
+
+ # now unload the migrations that have been defined
+ PeopleHaveLastNames.unloadable
+ ActiveSupport::Dependencies.remove_unloadable_constants!
+
+ ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", nil)
+
+ assert !defined? PeopleHaveLastNames
+
+ %w(WeNeedReminders, InnocentJointable).each do |migration|
+ assert defined? migration
+ end
+
+ ensure
+ load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb")
+ end
+
def test_migrator_interleaved_migrations
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_1")
@@ -1172,8 +1237,8 @@ if ActiveRecord::Base.connection.supports_migrations?
def test_timestamps_creates_updated_at_and_created_at
with_new_table do |t|
- t.expects(:column).with(:created_at, :datetime)
- t.expects(:column).with(:updated_at, :datetime)
+ t.expects(:column).with(:created_at, :datetime, kind_of(Hash))
+ t.expects(:column).with(:updated_at, :datetime, kind_of(Hash))
t.timestamps
end
end
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index eb3e43c8ac..7c3e0f2ca6 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -57,4 +57,29 @@ class MultipleDbTest < ActiveRecord::TestCase
assert Course.connection
end
+
+ def test_transactions_across_databases
+ c1 = Course.find(1)
+ e1 = Entrant.find(1)
+
+ begin
+ Course.transaction do
+ Entrant.transaction do
+ c1.name = "Typo"
+ e1.name = "Typo"
+ c1.save
+ e1.save
+ raise "No I messed up."
+ end
+ end
+ rescue
+ # Yup caught it
+ end
+
+ assert_equal "Typo", c1.name
+ assert_equal "Typo", e1.name
+
+ assert_equal "Ruby Development", Course.find(1).name
+ assert_equal "Ruby Developer", Entrant.find(1).name
+ end
end
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
index 0c1eb23428..6f6ea1cbe9 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/named_scope_test.rb
@@ -4,6 +4,7 @@ require 'models/topic'
require 'models/comment'
require 'models/reply'
require 'models/author'
+require 'models/developer'
class NamedScopeTest < ActiveRecord::TestCase
fixtures :posts, :authors, :topics, :comments, :author_addresses
@@ -45,6 +46,17 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count)
end
+ def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
+ assert Topic.approved.respond_to?(:proxy_found)
+ assert Topic.approved.respond_to?(:count)
+ assert Topic.approved.respond_to?(:length)
+ end
+
+ def test_respond_to_respects_include_private_parameter
+ assert !Topic.approved.respond_to?(:load_found)
+ assert Topic.approved.respond_to?(:load_found, true)
+ end
+
def test_subclasses_inherit_scopes
assert Topic.scopes.include?(:base)
@@ -184,6 +196,29 @@ class NamedScopeTest < ActiveRecord::TestCase
end
end
+ def test_any_should_not_load_results
+ topics = Topic.base
+ assert_queries(2) do
+ topics.any? # use count query
+ topics.collect # force load
+ topics.any? # use loaded (no query)
+ end
+ end
+
+ def test_any_should_call_proxy_found_if_using_a_block
+ topics = Topic.base
+ assert_queries(1) do
+ topics.expects(:empty?).never
+ topics.any? { true }
+ end
+ end
+
+ def test_any_should_not_fire_query_if_named_scope_loaded
+ topics = Topic.base
+ topics.collect # force load
+ assert_no_queries { assert topics.any? }
+ end
+
def test_should_build_with_proxy_options
topic = Topic.approved.build({})
assert topic.approved
@@ -209,4 +244,12 @@ class NamedScopeTest < ActiveRecord::TestCase
assert topic.approved
assert_equal 'lifo', topic.author_name
end
+
+ def test_find_all_should_behave_like_select
+ assert_equal Topic.base.select(&:approved), Topic.base.find_all(&:approved)
+ end
+
+ def test_should_use_where_in_query_for_named_scope
+ assert_equal Developer.find_all_by_name('Jamis'), Developer.find_all_by_id(Developer.jamises)
+ end
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index dc9eeec281..171d0e6dae 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -58,7 +58,7 @@ end
uses_mocha 'QueryCacheExpiryTest' do
class QueryCacheExpiryTest < ActiveRecord::TestCase
- fixtures :tasks
+ fixtures :tasks, :posts, :categories, :categories_posts
def test_find
Task.connection.expects(:clear_query_cache).times(1)
@@ -116,8 +116,9 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_cache_is_expired_by_habtm_delete
ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
ActiveRecord::Base.cache do
- c = Category.find(:first)
- p = Post.find(:first)
+ c = Category.find(1)
+ p = Post.find(1)
+ assert p.categories.any?
p.categories.delete_all
end
end
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index b921cbdc9c..b921cbdc9c 100755..100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 723062e3b8..4b86e32dbf 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -166,6 +166,10 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
end
+ def test_reflection_should_not_raise_error_when_compared_to_other_object
+ assert_nothing_raised { Firm.reflections[:clients] == Object.new }
+ end
+
private
def assert_reflection(klass, association, options)
assert reflection = klass.reflect_on_association(association)
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 06a76eacc3..af3ee6ddba 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -2,6 +2,7 @@ require "cases/helper"
require 'models/topic'
require 'models/reply'
require 'models/developer'
+require 'models/book'
class TransactionTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
@@ -86,8 +87,7 @@ class TransactionTest < ActiveRecord::TestCase
assert Topic.find(2).approved?, "Second should still be approved"
end
-
- def test_callback_rollback_in_save
+ def test_raising_exception_in_callback_rollbacks_in_save
add_exception_raising_after_save_callback_to_topic
begin
@@ -102,6 +102,54 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_cancellation_from_before_destroy_rollbacks_in_destroy
+ add_cancelling_before_destroy_with_db_side_effect_to_topic
+ begin
+ nbooks_before_destroy = Book.count
+ status = @first.destroy
+ assert !status
+ assert_nothing_raised(ActiveRecord::RecordNotFound) { @first.reload }
+ assert_equal nbooks_before_destroy, Book.count
+ ensure
+ remove_cancelling_before_destroy_with_db_side_effect_to_topic
+ end
+ end
+
+ def test_cancellation_from_before_filters_rollbacks_in_save
+ %w(validation save).each do |filter|
+ send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ begin
+ nbooks_before_save = Book.count
+ original_author_name = @first.author_name
+ @first.author_name += '_this_should_not_end_up_in_the_db'
+ status = @first.save
+ assert !status
+ assert_equal original_author_name, @first.reload.author_name
+ assert_equal nbooks_before_save, Book.count
+ ensure
+ send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ end
+ end
+ end
+
+ def test_cancellation_from_before_filters_rollbacks_in_save!
+ %w(validation save).each do |filter|
+ send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ begin
+ nbooks_before_save = Book.count
+ original_author_name = @first.author_name
+ @first.author_name += '_this_should_not_end_up_in_the_db'
+ @first.save!
+ flunk
+ rescue => e
+ assert_equal original_author_name, @first.reload.author_name
+ assert_equal nbooks_before_save, Book.count
+ ensure
+ send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ end
+ end
+ end
+
def test_callback_rollback_in_create
new_topic = Topic.new(
:title => "A new topic",
@@ -221,6 +269,16 @@ class TransactionTest < ActiveRecord::TestCase
def remove_exception_raising_after_create_callback_to_topic
Topic.class_eval { remove_method :after_create }
end
+
+ %w(validation save destroy).each do |filter|
+ define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
+ Topic.class_eval "def before_#{filter}() Book.create; false end"
+ end
+
+ define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
+ Topic.class_eval "remove_method :before_#{filter}"
+ end
+ end
end
if current_adapter?(:PostgreSQLAdapter)
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index 23ad10f3f9..23ad10f3f9 100755..100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb
new file mode 100644
index 0000000000..43592bcee3
--- /dev/null
+++ b/activerecord/test/cases/validations_i18n_test.rb
@@ -0,0 +1,858 @@
+require "cases/helper"
+require 'models/topic'
+require 'models/reply'
+
+class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
+ def setup
+ reset_callbacks Topic
+ @topic = Topic.new
+ I18n.backend.store_translations('en-US', :activerecord => {:errors => {:messages => {:custom => nil}}})
+ end
+
+ def teardown
+ reset_callbacks Topic
+ I18n.load_translations File.dirname(__FILE__) + '/../../lib/active_record/locale/en-US.yml'
+ end
+
+ def unique_topic
+ @unique ||= Topic.create :title => 'unique!'
+ end
+
+ def replied_topic
+ @replied_topic ||= begin
+ topic = Topic.create(:title => "topic")
+ topic.replies << Reply.new
+ topic
+ end
+ end
+
+ def reset_callbacks(*models)
+ models.each do |model|
+ model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ end
+ end
+
+ def test_default_error_messages_is_deprecated
+ assert_deprecated('ActiveRecord::Errors.default_error_messages') do
+ ActiveRecord::Errors.default_error_messages
+ end
+ end
+
+ # ActiveRecord::Errors
+ uses_mocha 'ActiveRecord::Errors' do
+
+ def test_errors_generate_message_translates_custom_model_attribute_key
+
+ I18n.expects(:translate).with(
+ :topic,
+ { :count => 1,
+ :default => ['Topic'],
+ :scope => [:activerecord, :models]
+ }
+ ).returns('Topic')
+
+ I18n.expects(:translate).with(
+ :"topic.title",
+ { :count => 1,
+ :default => ['Title'],
+ :scope => [:activerecord, :attributes]
+ }
+ ).returns('Title')
+
+ I18n.expects(:translate).with(
+ :"models.topic.attributes.title.invalid",
+ :value => nil,
+ :scope => [:activerecord, :errors],
+ :default => [
+ :"models.topic.invalid",
+ 'default from class def error 1',
+ :"messages.invalid"],
+ :attribute => "Title",
+ :model => "Topic"
+ ).returns('default from class def error 1')
+
+ @topic.errors.generate_message :title, :invalid, :default => 'default from class def error 1'
+ end
+
+ def test_errors_generate_message_translates_custom_model_attribute_keys_with_sti
+
+ I18n.expects(:translate).with(
+ :reply,
+ { :count => 1,
+ :default => [:topic, 'Reply'],
+ :scope => [:activerecord, :models]
+ }
+ ).returns('Reply')
+
+ I18n.expects(:translate).with(
+ :"reply.title",
+ { :count => 1,
+ :default => [:'topic.title', 'Title'],
+ :scope => [:activerecord, :attributes]
+ }
+ ).returns('Title')
+
+ I18n.expects(:translate).with(
+ :"models.reply.attributes.title.invalid",
+ :value => nil,
+ :scope => [:activerecord, :errors],
+ :default => [
+ :"models.reply.invalid",
+ :"models.topic.attributes.title.invalid",
+ :"models.topic.invalid",
+ 'default from class def',
+ :"messages.invalid"],
+ :model => 'Reply',
+ :attribute => 'Title'
+ ).returns("default from class def")
+
+ Reply.new.errors.generate_message :title, :invalid, :default => 'default from class def'
+
+ end
+
+ def test_errors_add_on_empty_generates_message
+ @topic.errors.expects(:generate_message).with(:title, :empty, {:default => nil})
+ @topic.errors.add_on_empty :title
+ end
+
+ def test_errors_add_on_empty_generates_message_with_custom_default_message
+ @topic.errors.expects(:generate_message).with(:title, :empty, {:default => 'custom'})
+ @topic.errors.add_on_empty :title, 'custom'
+ end
+
+ def test_errors_add_on_blank_generates_message
+ @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil})
+ @topic.errors.add_on_blank :title
+ end
+
+ def test_errors_add_on_blank_generates_message_with_custom_default_message
+ @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'})
+ @topic.errors.add_on_blank :title, 'custom'
+ end
+
+ def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
+ @topic.errors.instance_variable_set :@errors, { 'title' => ['empty'] }
+ I18n.expects(:translate).with(:"topic.title", :default => ['Title'], :scope => [:activerecord, :attributes], :count => 1).returns('Title')
+ @topic.errors.full_messages :locale => 'en-US'
+ end
+ end
+
+ # ActiveRecord::Validations
+ uses_mocha 'ActiveRecord::Validations' do
+ # validates_confirmation_of w/ mocha
+
+ def test_validates_confirmation_of_generates_message
+ Topic.validates_confirmation_of :title
+ @topic.title_confirmation = 'foo'
+ @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_confirmation_of_generates_message_with_custom_default_message
+ Topic.validates_confirmation_of :title, :message => 'custom'
+ @topic.title_confirmation = 'foo'
+ @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_acceptance_of w/ mocha
+
+ def test_validates_acceptance_of_generates_message
+ Topic.validates_acceptance_of :title, :allow_nil => false
+ @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_acceptance_of_generates_message_with_custom_default_message
+ Topic.validates_acceptance_of :title, :message => 'custom', :allow_nil => false
+ @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_presence_of w/ mocha
+
+ def test_validates_presence_of_generates_message
+ Topic.validates_presence_of :title
+ @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_presence_of_generates_message_with_custom_default_message
+ Topic.validates_presence_of :title, :message => 'custom'
+ @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'})
+ @topic.valid?
+ end
+
+ def test_validates_length_of_within_generates_message_with_title_too_short
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message
+ Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom'
+ @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'})
+ @topic.valid?
+ end
+
+ def test_validates_length_of_within_generates_message_with_title_too_long
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.title = 'this title is too long'
+ @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message
+ Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom'
+ @topic.title = 'this title is too long'
+ @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_length_of :within w/ mocha
+
+ def test_validates_length_of_within_generates_message_with_title_too_short
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message
+ Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom'
+ @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'})
+ @topic.valid?
+ end
+
+ def test_validates_length_of_within_generates_message_with_title_too_long
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.title = 'this title is too long'
+ @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message
+ Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom'
+ @topic.title = 'this title is too long'
+ @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_length_of :is w/ mocha
+
+ def test_validates_length_of_is_generates_message
+ Topic.validates_length_of :title, :is => 5
+ @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_length_of_is_generates_message_with_custom_default_message
+ Topic.validates_length_of :title, :is => 5, :message => 'custom'
+ @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_uniqueness_of w/ mocha
+
+ def test_validates_uniqueness_of_generates_message
+ Topic.validates_uniqueness_of :title
+ @topic.title = unique_topic.title
+ @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil, :value => 'unique!'})
+ @topic.valid?
+ end
+
+ def test_validates_uniqueness_of_generates_message_with_custom_default_message
+ Topic.validates_uniqueness_of :title, :message => 'custom'
+ @topic.title = unique_topic.title
+ @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom', :value => 'unique!'})
+ @topic.valid?
+ end
+
+ # validates_format_of w/ mocha
+
+ def test_validates_format_of_generates_message
+ Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
+ @topic.title = '72x'
+ @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_format_of_generates_message_with_custom_default_message
+ Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/, :message => 'custom'
+ @topic.title = '72x'
+ @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_inclusion_of w/ mocha
+
+ def test_validates_inclusion_of_generates_message
+ Topic.validates_inclusion_of :title, :in => %w(a b c)
+ @topic.title = 'z'
+ @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_inclusion_of_generates_message_with_custom_default_message
+ Topic.validates_inclusion_of :title, :in => %w(a b c), :message => 'custom'
+ @topic.title = 'z'
+ @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_exclusion_of w/ mocha
+
+ def test_validates_exclusion_of_generates_message
+ Topic.validates_exclusion_of :title, :in => %w(a b c)
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_exclusion_of_generates_message_with_custom_default_message
+ Topic.validates_exclusion_of :title, :in => %w(a b c), :message => 'custom'
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_numericality_of without :only_integer w/ mocha
+
+ def test_validates_numericality_of_generates_message
+ Topic.validates_numericality_of :title
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_numericality_of_generates_message_with_custom_default_message
+ Topic.validates_numericality_of :title, :message => 'custom'
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_numericality_of with :only_integer w/ mocha
+
+ def test_validates_numericality_of_only_integer_generates_message
+ Topic.validates_numericality_of :title, :only_integer => true
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message
+ Topic.validates_numericality_of :title, :only_integer => true, :message => 'custom'
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_numericality_of :odd w/ mocha
+
+ def test_validates_numericality_of_odd_generates_message
+ Topic.validates_numericality_of :title, :only_integer => true, :odd => true
+ @topic.title = 0
+ @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_numericality_of_odd_generates_message_with_custom_default_message
+ Topic.validates_numericality_of :title, :only_integer => true, :odd => true, :message => 'custom'
+ @topic.title = 0
+ @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_numericality_of :less_than w/ mocha
+
+ def test_validates_numericality_of_less_than_generates_message
+ Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
+ @topic.title = 1
+ @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => nil})
+ @topic.valid?
+ end
+
+ def test_validates_numericality_of_odd_generates_message_with_custom_default_message
+ Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0, :message => 'custom'
+ @topic.title = 1
+ @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'})
+ @topic.valid?
+ end
+
+ # validates_associated w/ mocha
+
+ def test_validates_associated_generates_message
+ Topic.validates_associated :replies
+ replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil})
+ replied_topic.valid?
+ end
+
+ def test_validates_associated_generates_message_with_custom_default_message
+ Topic.validates_associated :replies
+ replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil})
+ replied_topic.valid?
+ end
+ end
+
+ # validates_confirmation_of w/o mocha
+
+ def test_validates_confirmation_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:confirmation => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:confirmation => 'global message'}}}
+
+ Topic.validates_confirmation_of :title
+ @topic.title_confirmation = 'foo'
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_confirmation_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:confirmation => 'global message'}}}
+
+ Topic.validates_confirmation_of :title
+ @topic.title_confirmation = 'foo'
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_acceptance_of w/o mocha
+
+ def test_validates_acceptance_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:accepted => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:accepted => 'global message'}}}
+
+ Topic.validates_acceptance_of :title, :allow_nil => false
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_acceptance_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:accepted => 'global message'}}}
+
+ Topic.validates_acceptance_of :title, :allow_nil => false
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_presence_of w/o mocha
+
+ def test_validates_presence_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:blank => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:blank => 'global message'}}}
+
+ Topic.validates_presence_of :title
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_presence_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:blank => 'global message'}}}
+
+ Topic.validates_presence_of :title
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_length_of :within w/o mocha
+
+ def test_validates_length_of_within_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:too_short => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:too_short => 'global message'}}}
+
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_length_of_within_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:too_short => 'global message'}}}
+
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_length_of :is w/o mocha
+
+ def test_validates_length_of_within_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
+ Topic.validates_length_of :title, :is => 5
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_length_of_within_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
+ Topic.validates_length_of :title, :is => 5
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_uniqueness_of w/o mocha
+
+ def test_validates_length_of_within_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
+ Topic.validates_length_of :title, :is => 5
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_length_of_within_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
+ Topic.validates_length_of :title, :is => 5
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+
+ # validates_format_of w/o mocha
+
+ def test_validates_format_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:invalid => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
+ Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_format_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
+ Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_inclusion_of w/o mocha
+
+ def test_validates_inclusion_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:inclusion => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:inclusion => 'global message'}}}
+
+ Topic.validates_inclusion_of :title, :in => %w(a b c)
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_inclusion_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:inclusion => 'global message'}}}
+
+ Topic.validates_inclusion_of :title, :in => %w(a b c)
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_exclusion_of w/o mocha
+
+ def test_validates_exclusion_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:exclusion => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:exclusion => 'global message'}}}
+
+ Topic.validates_exclusion_of :title, :in => %w(a b c)
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_exclusion_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:exclusion => 'global message'}}}
+
+ Topic.validates_exclusion_of :title, :in => %w(a b c)
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_numericality_of without :only_integer w/o mocha
+
+ def test_validates_numericality_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
+ Topic.validates_numericality_of :title
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_numericality_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_numericality_of with :only_integer w/o mocha
+
+ def test_validates_numericality_of_only_integer_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_numericality_of_only_integer_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_numericality_of :odd w/o mocha
+
+ def test_validates_numericality_of_odd_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:odd => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:odd => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true, :odd => true
+ @topic.title = 0
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_numericality_of_odd_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:odd => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true, :odd => true
+ @topic.title = 0
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_numericality_of :less_than w/o mocha
+
+ def test_validates_numericality_of_less_than_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:less_than => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:less_than => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
+ @topic.title = 1
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_numericality_of_less_than_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:less_than => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
+ @topic.title = 1
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+
+ # validates_associated w/o mocha
+
+ def test_validates_associated_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
+ Topic.validates_associated :replies
+ replied_topic.valid?
+ assert_equal 'custom message', replied_topic.errors.on(:replies)
+ end
+
+ def test_validates_associated_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
+ Topic.validates_associated :replies
+ replied_topic.valid?
+ assert_equal 'global message', replied_topic.errors.on(:replies)
+ end
+end
+
+class ActiveRecordValidationsGenerateMessageI18nTests < Test::Unit::TestCase
+ def setup
+ reset_callbacks Topic
+ @topic = Topic.new
+ I18n.backend.store_translations :'en-US', {
+ :activerecord => {
+ :errors => {
+ :messages => {
+ :inclusion => "is not included in the list",
+ :exclusion => "is reserved",
+ :invalid => "is invalid",
+ :confirmation => "doesn't match confirmation",
+ :accepted => "must be accepted",
+ :empty => "can't be empty",
+ :blank => "can't be blank",
+ :too_long => "is too long (maximum is {{count}} characters)",
+ :too_short => "is too short (minimum is {{count}} characters)",
+ :wrong_length => "is the wrong length (should be {{count}} characters)",
+ :taken => "has already been taken",
+ :not_a_number => "is not a number",
+ :greater_than => "must be greater than {{count}}",
+ :greater_than_or_equal_to => "must be greater than or equal to {{count}}",
+ :equal_to => "must be equal to {{count}}",
+ :less_than => "must be less than {{count}}",
+ :less_than_or_equal_to => "must be less than or equal to {{count}}",
+ :odd => "must be odd",
+ :even => "must be even"
+ }
+ }
+ }
+ }
+ end
+
+ def reset_callbacks(*models)
+ models.each do |model|
+ model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ end
+ end
+
+ # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value)
+ def test_generate_message_inclusion_with_default_message
+ assert_equal 'is not included in the list', @topic.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_inclusion_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value)
+ def test_generate_message_exclusion_with_default_message
+ assert_equal 'is reserved', @topic.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_exclusion_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
+ # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
+ def test_generate_message_invalid_with_default_message
+ assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_invalid_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message])
+ def test_generate_message_confirmation_with_default_message
+ assert_equal "doesn't match confirmation", @topic.errors.generate_message(:title, :confirmation, :default => nil)
+ end
+
+ def test_generate_message_confirmation_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :confirmation, :default => 'custom message')
+ end
+
+ # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message])
+ def test_generate_message_accepted_with_default_message
+ assert_equal "must be accepted", @topic.errors.generate_message(:title, :accepted, :default => nil)
+ end
+
+ def test_generate_message_accepted_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :accepted, :default => 'custom message')
+ end
+
+ # add_on_empty: generate_message(attr, :empty, :default => custom_message)
+ def test_generate_message_empty_with_default_message
+ assert_equal "can't be empty", @topic.errors.generate_message(:title, :empty, :default => nil)
+ end
+
+ def test_generate_message_empty_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :empty, :default => 'custom message')
+ end
+
+ # add_on_blank: generate_message(attr, :blank, :default => custom_message)
+ def test_generate_message_blank_with_default_message
+ assert_equal "can't be blank", @topic.errors.generate_message(:title, :blank, :default => nil)
+ end
+
+ def test_generate_message_blank_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :blank, :default => 'custom message')
+ end
+
+ # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end)
+ def test_generate_message_too_long_with_default_message
+ assert_equal "is too long (maximum is 10 characters)", @topic.errors.generate_message(:title, :too_long, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_too_long_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
+ def test_generate_message_too_short_with_default_message
+ assert_equal "is too short (minimum is 10 characters)", @topic.errors.generate_message(:title, :too_short, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_too_short_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value)
+ def test_generate_message_wrong_length_with_default_message
+ assert_equal "is the wrong length (should be 10 characters)", @topic.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_wrong_length_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message])
+ def test_generate_message_taken_with_default_message
+ assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_taken_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
+ def test_generate_message_not_a_number_with_default_message
+ assert_equal "is not a number", @topic.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_not_a_number_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message])
+ def test_generate_message_greater_than_with_default_message
+ assert_equal "must be greater than 10", @topic.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_greater_than_or_equal_to_with_default_message
+ assert_equal "must be greater than or equal to 10", @topic.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_equal_to_with_default_message
+ assert_equal "must be equal to 10", @topic.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_less_than_with_default_message
+ assert_equal "must be less than 10", @topic.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_less_than_or_equal_to_with_default_message
+ assert_equal "must be less than or equal to 10", @topic.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_odd_with_default_message
+ assert_equal "must be odd", @topic.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_even_with_default_message
+ assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10)
+ end
+end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 0742e2c632..a40bda2533 100755..100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -477,6 +477,15 @@ class ValidationsTest < ActiveRecord::TestCase
assert_not_equal "has already been taken", t3.errors.on(:title)
end
+ def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer
+ Topic.validates_uniqueness_of(:title, :case_sensitve => true)
+ t = Topic.create!('title' => 101)
+
+ t2 = Topic.new('title' => 101)
+ assert !t2.valid?
+ assert t2.errors.on(:title)
+ end
+
def test_validate_uniqueness_with_non_standard_table_names
i1 = WarehouseThing.create(:value => 1000)
assert !i1.valid?, "i1 should not be valid"
@@ -853,7 +862,9 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validates_length_with_globally_modified_error_message
- ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d'
+ ActiveSupport::Deprecation.silence do
+ ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d'
+ end
Topic.validates_length_of :title, :minimum => 10
t = Topic.create(:title => 'too short')
assert !t.valid?
@@ -1409,8 +1420,8 @@ class ValidatesNumericalityTest < ActiveRecord::TestCase
def test_validates_numericality_of_with_nil_allowed
Topic.validates_numericality_of :approved, :allow_nil => true
- invalid!(BLANK + JUNK)
- valid!(NIL + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ invalid!(JUNK)
+ valid!(NIL + BLANK + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
end
def test_validates_numericality_of_with_integer_only
@@ -1423,8 +1434,8 @@ class ValidatesNumericalityTest < ActiveRecord::TestCase
def test_validates_numericality_of_with_integer_only_and_nil_allowed
Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true
- invalid!(BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY)
- valid!(NIL + INTEGERS)
+ invalid!(JUNK + FLOATS + BIGDECIMAL + INFINITY)
+ valid!(NIL + BLANK + INTEGERS)
end
def test_validates_numericality_with_greater_than
diff --git a/activerecord/test/connections/native_mysql/connection.rb b/activerecord/test/connections/native_mysql/connection.rb
index 1fab444e58..140e06d631 100644
--- a/activerecord/test/connections/native_mysql/connection.rb
+++ b/activerecord/test/connections/native_mysql/connection.rb
@@ -2,9 +2,7 @@ print "Using native MySQL\n"
require_dependency 'models/course'
require 'logger'
-RAILS_DEFAULT_LOGGER = Logger.new('debug.log')
-RAILS_DEFAULT_LOGGER.level = Logger::DEBUG
-ActiveRecord::Base.logger = RAILS_DEFAULT_LOGGER
+ActiveRecord::Base.logger = Logger.new("debug.log")
# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost';
# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost';
diff --git a/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb b/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb
new file mode 100644
index 0000000000..ffb224dad9
--- /dev/null
+++ b/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb
@@ -0,0 +1,10 @@
+class MigrationThatRaisesException < ActiveRecord::Migration
+ def self.up
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+
+ def self.down
+ remove_column "people", "last_name"
+ end
+end
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 136dc39cf3..c6aa0293c2 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -32,6 +32,9 @@ class Author < ActiveRecord::Base
has_many :special_posts
has_many :special_post_comments, :through => :special_posts, :source => :comments
+ has_many :sti_posts, :class_name => 'StiPost'
+ has_many :sti_post_comments, :through => :sti_posts, :source => :comments
+
has_many :special_nonexistant_posts, :class_name => "SpecialPost", :conditions => "posts.body = 'nonexistant'"
has_many :special_nonexistant_post_comments, :through => :special_nonexistant_posts, :source => :comments, :conditions => "comments.post_id = 0"
has_many :nonexistant_comments, :through => :posts
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index e6aa810146..cd435948a1 100755..100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -18,6 +18,13 @@ end
module Namespaced
class Company < ::Company
end
+
+ class Firm < ::Company
+ has_many :clients, :class_name => 'Namespaced::Client'
+ end
+
+ class Client < ::Company
+ end
end
class Firm < Company
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 9f26cacdec..c08476f728 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -43,6 +43,8 @@ class Developer < ActiveRecord::Base
has_many :audit_logs
+ named_scope :jamises, :conditions => {:name => 'Jamis'}
+
validates_inclusion_of :salary, :in => 50000..200000
validates_length_of :name, :within => 3..20
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index e1ab89eca5..44c692b5e7 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -7,7 +7,7 @@ class Project < ActiveRecord::Base
has_and_belongs_to_many :developers_named_david, :class_name => "Developer", :conditions => "name = 'David'", :uniq => true
has_and_belongs_to_many :developers_named_david_with_hash_conditions, :class_name => "Developer", :conditions => { :name => 'David' }, :uniq => true
has_and_belongs_to_many :salaried_developers, :class_name => "Developer", :conditions => "salary > 0"
- has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => 'SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id}'
+ has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => 'SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id'
has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => "DELETE FROM developers_projects WHERE project_id = \#{id} AND developer_id = \#{record.id}"
has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || '<new>'}"},
:after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"},
diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb
index 812bc1f535..812bc1f535 100755..100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 39ca1bf42a..39ca1bf42a 100755..100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb
index 5ae062c97c..f44c33ae67 100644
--- a/activerecord/test/schema/mysql_specific_schema.rb
+++ b/activerecord/test/schema/mysql_specific_schema.rb
@@ -1,5 +1,5 @@
ActiveRecord::Schema.define do
- create_table :binary_fields, :force => true do |t|
+ create_table :binary_fields, :force => true, :options => 'CHARACTER SET latin1' do |t|
t.binary :tiny_blob, :limit => 255
t.binary :normal_blob, :limit => 65535
t.binary :medium_blob, :limit => 16777215
@@ -9,4 +9,4 @@ ActiveRecord::Schema.define do
t.text :medium_text, :limit => 16777215
t.text :long_text, :limit => 2147483647
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 29c91a4464..ab5c7c520b 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -60,12 +60,13 @@ ActiveRecord::Schema.define do
end
create_table :booleantests, :force => true do |t|
- t.integer :value
+ t.boolean :value
end
create_table :categories, :force => true do |t|
t.string :name, :null => false
t.string :type
+ t.integer :categorizations_count
end
create_table :categories_posts, :force => true, :id => false do |t|
diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG
index 673e20de28..012f9028a0 100644
--- a/activeresource/CHANGELOG
+++ b/activeresource/CHANGELOG
@@ -1,5 +1,9 @@
*Edge*
+* Add ActiveResource::Base.find(:last). [#754 state:resolved] (Adrian Mugnolo)
+
+* Fixed problems with the logger used if the logging string included %'s [#840 state:resolved] (Jamis Buck)
+
* Fixed Base#exists? to check status code as integer [#299 state:resolved] (Wes Oldenbeuving)
diff --git a/activeresource/README b/activeresource/README
index bcb7b3cbc7..924017a659 100644
--- a/activeresource/README
+++ b/activeresource/README
@@ -37,7 +37,7 @@ lifecycle methods that operate against a persistent store.
Person.exists?(1) #=> true
As you can see, the methods are quite similar to Active Record's methods for dealing with database
-records. But rather than dealing with
+records. But rather than dealing directly with a database record, you're dealing with HTTP resources (which may or may not be database records).
==== Protocol
diff --git a/activeresource/lib/active_resource.rb b/activeresource/lib/active_resource.rb
index 18347457aa..db9007060f 100644
--- a/activeresource/lib/active_resource.rb
+++ b/activeresource/lib/active_resource.rb
@@ -21,16 +21,13 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-$:.unshift(File.dirname(__FILE__)) unless
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
-
-unless defined?(ActiveSupport)
- begin
- $:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
+begin
+ require 'active_support'
+rescue LoadError
+ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
+ if File.directory?(activesupport_path)
+ $:.unshift activesupport_path
require 'active_support'
- rescue LoadError
- require 'rubygems'
- gem 'activesupport'
end
end
@@ -44,4 +41,4 @@ module ActiveResource
include Validations
include CustomMethods
end
-end \ No newline at end of file
+end
diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb
index 347dbb82aa..4af30ea13f 100644
--- a/activeresource/lib/active_resource/base.rb
+++ b/activeresource/lib/active_resource/base.rb
@@ -13,43 +13,43 @@ module ActiveResource
# to Ruby objects, Active Resource only needs a class name that corresponds to the resource name (e.g., the class
# Person maps to the resources people, very similarly to Active Record) and a +site+ value, which holds the
# URI of the resources.
- #
+ #
# class Person < ActiveResource::Base
# self.site = "http://api.people.com:3000/"
# end
- #
+ #
# Now the Person class is mapped to RESTful resources located at <tt>http://api.people.com:3000/people/</tt>, and
- # you can now use Active Resource's lifecycles methods to manipulate resources. In the case where you already have
+ # you can now use Active Resource's lifecycles methods to manipulate resources. In the case where you already have
# an existing model with the same name as the desired RESTful resource you can set the +element_name+ value.
#
# class PersonResource < ActiveResource::Base
# self.site = "http://api.people.com:3000/"
# self.element_name = "person"
# end
- #
- #
+ #
+ #
# == Lifecycle methods
#
# Active Resource exposes methods for creating, finding, updating, and deleting resources
# from REST web services.
- #
+ #
# ryan = Person.new(:first => 'Ryan', :last => 'Daigle')
# ryan.save # => true
# ryan.id # => 2
# Person.exists?(ryan.id) # => true
# ryan.exists? # => true
- #
+ #
# ryan = Person.find(1)
# # Resource holding our newly created Person object
- #
+ #
# ryan.first = 'Rizzle'
# ryan.save # => true
- #
+ #
# ryan.destroy # => true
#
# As you can see, these are very similar to Active Record's lifecycle methods for database records.
# You can read more about each of these methods in their respective documentation.
- #
+ #
# === Custom REST methods
#
# Since simple CRUD/lifecycle methods can't accomplish every task, Active Resource also supports
@@ -71,14 +71,14 @@ module ActiveResource
#
# # DELETE to 'fire' a person, i.e. DELETE /people/1/fire.xml.
# Person.find(1).delete(:fire)
- #
+ #
# For more information on using custom REST methods, see the
# ActiveResource::CustomMethods documentation.
#
# == Validations
#
# You can validate resources client side by overriding validation methods in the base class.
- #
+ #
# class Person < ActiveResource::Base
# self.site = "http://api.people.com:3000/"
# protected
@@ -86,19 +86,19 @@ module ActiveResource
# errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/
# end
# end
- #
+ #
# See the ActiveResource::Validations documentation for more information.
#
# == Authentication
- #
+ #
# Many REST APIs will require authentication, usually in the form of basic
# HTTP authentication. Authentication can be specified by:
# * putting the credentials in the URL for the +site+ variable.
- #
+ #
# class Person < ActiveResource::Base
# self.site = "http://ryan:password@api.people.com:3000/"
# end
- #
+ #
# * defining +user+ and/or +password+ variables
#
# class Person < ActiveResource::Base
@@ -107,29 +107,29 @@ module ActiveResource
# self.password = "password"
# end
#
- # For obvious security reasons, it is probably best if such services are available
+ # For obvious security reasons, it is probably best if such services are available
# over HTTPS.
- #
- # Note: Some values cannot be provided in the URL passed to site. e.g. email addresses
- # as usernames. In those situations you should use the seperate user and password option.
+ #
+ # Note: Some values cannot be provided in the URL passed to site. e.g. email addresses
+ # as usernames. In those situations you should use the separate user and password option.
# == Errors & Validation
#
# Error handling and validation is handled in much the same manner as you're used to seeing in
# Active Record. Both the response code in the HTTP response and the body of the response are used to
# indicate that an error occurred.
- #
+ #
# === Resource errors
- #
+ #
# When a GET is requested for a resource that does not exist, the HTTP <tt>404</tt> (Resource Not Found)
# response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
# exception.
- #
+ #
# # GET http://api.people.com:3000/people/999.xml
# ryan = Person.find(999) # 404, raises ActiveResource::ResourceNotFound
- #
+ #
# <tt>404</tt> is just one of the HTTP error response codes that Active Resource will handle with its own exception. The
# following HTTP response codes will also result in these exceptions:
- #
+ #
# * 200..399 - Valid response, no exception
# * 404 - ActiveResource::ResourceNotFound
# * 409 - ActiveResource::ResourceConflict
@@ -149,17 +149,17 @@ module ActiveResource
# end
#
# === Validation errors
- #
+ #
# Active Resource supports validations on resources and will return errors if any these validations fail
- # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by
+ # (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by
# a response code of <tt>422</tt> and an XML representation of the validation errors. The save operation will
# then fail (with a <tt>false</tt> return value) and the validation errors can be accessed on the resource in question.
- #
+ #
# ryan = Person.find(1)
# ryan.first # => ''
# ryan.save # => false
#
- # # When
+ # # When
# # PUT http://api.people.com:3000/people/1.xml
# # is requested with invalid values, the response is:
# #
@@ -169,7 +169,7 @@ module ActiveResource
#
# ryan.errors.invalid?(:first) # => true
# ryan.errors.full_messages # => ['First cannot be empty']
- #
+ #
# Learn more about Active Resource's validation features in the ActiveResource::Validations documentation.
#
# === Timeouts
@@ -200,7 +200,7 @@ module ActiveResource
cattr_accessor :logger
class << self
- # Gets the URI of the REST resources to map for this class. The site variable is required
+ # Gets the URI of the REST resources to map for this class. The site variable is required for
# Active Resource's mapping to work.
def site
# Not using superclass_delegating_reader because don't want subclasses to modify superclass instance
@@ -226,7 +226,7 @@ module ActiveResource
end
# Sets the URI of the REST resources to map for this class to the value in the +site+ argument.
- # The site variable is required Active Resource's mapping to work.
+ # The site variable is required for Active Resource's mapping to work.
def site=(site)
@connection = nil
if site.nil?
@@ -280,7 +280,7 @@ module ActiveResource
#
# Default format is <tt>:xml</tt>.
def format=(mime_type_reference_or_format)
- format = mime_type_reference_or_format.is_a?(Symbol) ?
+ format = mime_type_reference_or_format.is_a?(Symbol) ?
ActiveResource::Formats[mime_type_reference_or_format] : mime_type_reference_or_format
write_inheritable_attribute("format", format)
@@ -288,7 +288,7 @@ module ActiveResource
end
# Returns the current format, default is ActiveResource::Formats::XmlFormat.
- def format # :nodoc:
+ def format
read_inheritable_attribute("format") || ActiveResource::Formats[:xml]
end
@@ -298,7 +298,7 @@ module ActiveResource
@timeout = timeout
end
- # Gets tthe number of seconds after which requests to the REST API should time out.
+ # Gets the number of seconds after which requests to the REST API should time out.
def timeout
if defined?(@timeout)
@timeout
@@ -332,7 +332,7 @@ module ActiveResource
attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
attr_accessor_with_default(:primary_key, 'id') #:nodoc:
-
+
# Gets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>)
# This method is regenerated at runtime based on what the prefix is set to.
def prefix(options={})
@@ -356,6 +356,9 @@ module ActiveResource
# Replace :placeholders with '#{embedded options[:lookups]}'
prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" }
+ # Clear prefix parameters in case they have been cached
+ @prefix_parameters = nil
+
# Redefine the new methods.
code = <<-end_code
def prefix_source() "#{value}" end
@@ -381,21 +384,21 @@ module ActiveResource
# +query_options+ - A hash to add items to the query string for the request.
#
# ==== Examples
- # Post.element_path(1)
+ # Post.element_path(1)
# # => /posts/1.xml
#
- # Comment.element_path(1, :post_id => 5)
+ # Comment.element_path(1, :post_id => 5)
# # => /posts/5/comments/1.xml
#
- # Comment.element_path(1, :post_id => 5, :active => 1)
+ # Comment.element_path(1, :post_id => 5, :active => 1)
# # => /posts/5/comments/1.xml?active=1
#
- # Comment.element_path(1, {:post_id => 5}, {:active => 1})
+ # Comment.element_path(1, {:post_id => 5}, {:active => 1})
# # => /posts/5/comments/1.xml?active=1
#
def element_path(id, prefix_options = {}, query_options = nil)
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
- "#{prefix(prefix_options)}#{collection_name}/#{id}.#{format.extension}#{query_string(query_options)}"
+ "#{prefix(prefix_options)}#{collection_name}/#{id}.#{format.extension}#{query_string(query_options)}"
end
# Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails
@@ -410,13 +413,13 @@ module ActiveResource
# Post.collection_path
# # => /posts.xml
#
- # Comment.collection_path(:post_id => 5)
+ # Comment.collection_path(:post_id => 5)
# # => /posts/5/comments.xml
#
- # Comment.collection_path(:post_id => 5, :active => 1)
+ # Comment.collection_path(:post_id => 5, :active => 1)
# # => /posts/5/comments.xml?active=1
#
- # Comment.collection_path({:post_id => 5}, {:active => 1})
+ # Comment.collection_path({:post_id => 5}, {:active => 1})
# # => /posts/5/comments.xml?active=1
#
def collection_path(prefix_options = {}, query_options = nil)
@@ -426,16 +429,16 @@ module ActiveResource
alias_method :set_primary_key, :primary_key= #:nodoc:
- # Create a new resource instance and request to the remote service
+ # Creates a new resource instance and makes a request to the remote service
# that it be saved, making it equivalent to the following simultaneous calls:
#
# ryan = Person.new(:first => 'ryan')
# ryan.save
#
- # The newly created resource is returned. If a failure has occurred an
- # exception will be raised (see save). If the resource is invalid and
- # has not been saved then valid? will return <tt>false</tt>,
- # while new? will still return <tt>true</tt>.
+ # Returns the newly created resource. If a failure has occurred an
+ # exception will be raised (see <tt>save</tt>). If the resource is invalid and
+ # has not been saved then <tt>valid?</tt> will return <tt>false</tt>,
+ # while <tt>new?</tt> will still return <tt>true</tt>.
#
# ==== Examples
# Person.create(:name => 'Jeremy', :email => 'myname@nospam.com', :enabled => true)
@@ -451,50 +454,54 @@ module ActiveResource
# that_guy.valid? # => false
# that_guy.new? # => true
def create(attributes = {})
- returning(self.new(attributes)) { |res| res.save }
+ returning(self.new(attributes)) { |res| res.save }
end
# Core method for finding resources. Used similarly to Active Record's +find+ method.
#
# ==== Arguments
- # The first argument is considered to be the scope of the query. That is, how many
+ # The first argument is considered to be the scope of the query. That is, how many
# resources are returned from the request. It can be one of the following.
#
# * <tt>:one</tt> - Returns a single resource.
# * <tt>:first</tt> - Returns the first resource found.
+ # * <tt>:last</tt> - Returns the last resource found.
# * <tt>:all</tt> - Returns every resource that matches the request.
- #
+ #
# ==== Options
#
# * <tt>:from</tt> - Sets the path or custom method that resources will be fetched from.
# * <tt>:params</tt> - Sets query and prefix (nested URL) parameters.
#
# ==== Examples
- # Person.find(1)
+ # Person.find(1)
# # => GET /people/1.xml
#
- # Person.find(:all)
+ # Person.find(:all)
# # => GET /people.xml
#
- # Person.find(:all, :params => { :title => "CEO" })
+ # Person.find(:all, :params => { :title => "CEO" })
# # => GET /people.xml?title=CEO
#
- # Person.find(:first, :from => :managers)
+ # Person.find(:first, :from => :managers)
# # => GET /people/managers.xml
#
- # Person.find(:all, :from => "/companies/1/people.xml")
+ # Person.find(:last, :from => :managers)
+ # # => GET /people/managers.xml
+ #
+ # Person.find(:all, :from => "/companies/1/people.xml")
# # => GET /companies/1/people.xml
#
- # Person.find(:one, :from => :leader)
+ # Person.find(:one, :from => :leader)
# # => GET /people/leader.xml
#
# Person.find(:all, :from => :developers, :params => { :language => 'ruby' })
# # => GET /people/developers.xml?language=ruby
#
- # Person.find(:one, :from => "/companies/1/manager.xml")
+ # Person.find(:one, :from => "/companies/1/manager.xml")
# # => GET /companies/1/manager.xml
#
- # StreetAddress.find(1, :params => { :person_id => 1 })
+ # StreetAddress.find(1, :params => { :person_id => 1 })
# # => GET /people/1/street_addresses/1.xml
def find(*arguments)
scope = arguments.slice!(0)
@@ -503,6 +510,7 @@ module ActiveResource
case scope
when :all then find_every(options)
when :first then find_every(options).first
+ when :last then find_every(options).last
when :one then find_one(options)
else find_single(scope, options)
end
@@ -560,7 +568,7 @@ module ActiveResource
instantiate_collection( (connection.get(path, headers) || []), prefix_options )
end
end
-
+
# Find a single resource from a one-off URL
def find_one(options)
case from = options[:from]
@@ -578,7 +586,7 @@ module ActiveResource
path = element_path(scope, prefix_options, query_options)
instantiate_record(connection.get(path, headers), prefix_options)
end
-
+
def instantiate_collection(collection, prefix_options = {})
collection.collect! { |record| instantiate_record(record, prefix_options) }
end
@@ -602,10 +610,10 @@ module ActiveResource
# Builds the query string for the request.
def query_string(options)
- "?#{options.to_query}" unless options.nil? || options.empty?
+ "?#{options.to_query}" unless options.nil? || options.empty?
end
- # split an option hash into two hashes, one containing the prefix options,
+ # split an option hash into two hashes, one containing the prefix options,
# and the other containing the leftovers.
def split_options(options = {})
prefix_options, query_options = {}, {}
@@ -654,7 +662,7 @@ module ActiveResource
# ryan = Person.find(1)
# ryan.address = StreetAddress.find(1, :person_id => ryan.id)
# ryan.hash = {:not => "an ARes instance"}
- #
+ #
# not_ryan = ryan.clone
# not_ryan.new? # => true
# not_ryan.address # => NoMethodError
@@ -706,7 +714,7 @@ module ActiveResource
id && id.to_s
end
- # Test for equality. Resource are equal if and only if +other+ is the same object or
+ # Test for equality. Resource are equal if and only if +other+ is the same object or
# is an instance of the same class, is not <tt>new?</tt>, and has the same +id+.
#
# ==== Examples
@@ -742,7 +750,7 @@ module ActiveResource
def hash
id.hash
end
-
+
# Duplicate the current resource without saving it.
#
# ==== Examples
@@ -762,7 +770,7 @@ module ActiveResource
end
end
- # A method to save (+POST+) or update (+PUT+) a resource. It delegates to +create+ if a new object,
+ # A method to save (+POST+) or update (+PUT+) a resource. It delegates to +create+ if a new object,
# +update+ if it is existing. If the response to the save includes a body, it will be assumed that this body
# is XML for the final object as it looked after the save (which would include attributes like +created_at+
# that weren't part of the original submit).
@@ -786,7 +794,7 @@ module ActiveResource
# my_person = Person.find(my_id)
# my_person.destroy
# Person.find(my_id) # 404 (Resource Not Found)
- #
+ #
# new_person = Person.create(:name => 'James')
# new_id = new_person.id # => 7
# new_person.destroy
@@ -812,7 +820,7 @@ module ActiveResource
# Person.delete(guys_id)
# that_guy.exists? # => false
def exists?
- !new? && self.class.exists?(to_param, :params => prefix_options)
+ !new? && self.class.exists?(to_param, :params => prefix_options)
end
# A method to convert the the resource to an XML string.
@@ -825,7 +833,7 @@ module ActiveResource
# * <tt>:indent</tt> - Set the indent level for the XML output (default is +2+).
# * <tt>:dasherize</tt> - Boolean option to determine whether or not element names should
# replace underscores with dashes (default is <tt>false</tt>).
- # * <tt>:skip_instruct</tt> - Toggle skipping the +instruct!+ call on the XML builder
+ # * <tt>:skip_instruct</tt> - Toggle skipping the +instruct!+ call on the XML builder
# that generates the XML declaration (default is <tt>false</tt>).
#
# ==== Examples
@@ -849,7 +857,7 @@ module ActiveResource
# ==== Examples
# my_branch = Branch.find(:first)
# my_branch.name # => "Wislon Raod"
- #
+ #
# # Another client fixes the typo...
#
# my_branch.name # => "Wislon Raod"
@@ -897,7 +905,7 @@ module ActiveResource
end
self
end
-
+
# For checking <tt>respond_to?</tt> without searching the attributes (which is faster).
alias_method :respond_to_without_attributes?, :respond_to?
@@ -909,7 +917,7 @@ module ActiveResource
if attributes.nil?
return super
elsif attributes.has_key?(method_name)
- return true
+ return true
elsif ['?','='].include?(method_name.last) && attributes.has_key?(method_name.first(-1))
return true
end
@@ -917,7 +925,7 @@ module ActiveResource
# would return true for generated readers, even if the attribute wasn't present
super
end
-
+
protected
def connection(refresh = false)
@@ -938,7 +946,7 @@ module ActiveResource
load_attributes_from_response(response)
end
end
-
+
def load_attributes_from_response(response)
if response['Content-Length'] != "0" && response.body.strip.size > 0
load(self.class.format.decode(response.body))
@@ -963,7 +971,7 @@ module ActiveResource
def find_or_create_resource_for_collection(name)
find_or_create_resource_for(name.to_s.singularize)
end
-
+
# Tries to find a resource in a non empty list of nested modules
# Raises a NameError if it was not found in any of the given nested modules
def find_resource_in_modules(resource_name, module_names)
diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb
index 0c4ea432d7..d64fb79f1e 100644
--- a/activeresource/lib/active_resource/connection.rb
+++ b/activeresource/lib/active_resource/connection.rb
@@ -28,24 +28,24 @@ module ActiveResource
# 3xx Redirection
class Redirection < ConnectionError # :nodoc:
- def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
- end
+ def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
+ end
# 4xx Client Error
class ClientError < ConnectionError; end # :nodoc:
-
+
# 400 Bad Request
class BadRequest < ClientError; end # :nodoc
-
+
# 401 Unauthorized
class UnauthorizedAccess < ClientError; end # :nodoc
-
+
# 403 Forbidden
class ForbiddenAccess < ClientError; end # :nodoc
-
+
# 404 Not Found
class ResourceNotFound < ClientError; end # :nodoc:
-
+
# 409 Conflict
class ResourceConflict < ClientError; end # :nodoc:
@@ -140,7 +140,7 @@ module ActiveResource
logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
result = nil
time = Benchmark.realtime { result = http.send(method, path, *arguments) }
- logger.info "--> #{result.code} #{result.message} (#{result.body ? result.body.length : 0}b %.2fs)" % time if logger
+ logger.info "--> %d %s (%d %.2fs)" % [result.code, result.message, result.body ? result.body.length : 0, time] if logger
handle_response(result)
rescue Timeout::Error => e
raise TimeoutError.new(e.message)
@@ -201,7 +201,7 @@ module ActiveResource
end
def logger #:nodoc:
- ActiveResource::Base.logger
+ Base.logger
end
end
end
diff --git a/activeresource/lib/active_resource/custom_methods.rb b/activeresource/lib/active_resource/custom_methods.rb
index 4c8699288c..770116ceb7 100644
--- a/activeresource/lib/active_resource/custom_methods.rb
+++ b/activeresource/lib/active_resource/custom_methods.rb
@@ -48,8 +48,8 @@ module ActiveResource
# # => [{:id => 1, :name => 'Ryan'}]
#
# Note: the objects returned from this method are not automatically converted
- # into Active Resource instances - they are ordinary Hashes. If you are expecting
- # Active Resource instances, use the <tt>find</tt> class method with the
+ # into ActiveResource::Base instances - they are ordinary Hashes. If you are expecting
+ # ActiveResource::Base instances, use the <tt>find</tt> class method with the
# <tt>:from</tt> option. For example:
#
# Person.find(:all, :from => :active)
diff --git a/activeresource/lib/active_resource/http_mock.rb b/activeresource/lib/active_resource/http_mock.rb
index 22f83ae910..554fc3bcfc 100644
--- a/activeresource/lib/active_resource/http_mock.rb
+++ b/activeresource/lib/active_resource/http_mock.rb
@@ -65,7 +65,7 @@ module ActiveResource
class << self
# Returns an array of all request objects that have been sent to the mock. You can use this to check
- # wether or not your model actually sent an HTTP request.
+ # if your model actually sent an HTTP request.
#
# ==== Example
# def setup
diff --git a/activeresource/test/abstract_unit.rb b/activeresource/test/abstract_unit.rb
index 615a6d9222..e612412033 100644
--- a/activeresource/test/abstract_unit.rb
+++ b/activeresource/test/abstract_unit.rb
@@ -9,14 +9,18 @@ require 'setter_trap'
ActiveResource::Base.logger = Logger.new("#{File.dirname(__FILE__)}/debug.log")
+def uses_gem(gem_name, test_name, version = '> 0')
+ require 'rubygems'
+ gem gem_name.to_s, version
+ require gem_name.to_s
+ yield
+rescue LoadError
+ $stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again."
+end
+
# Wrap tests that use Mocha and skip if unavailable.
-def uses_mocha(test_name)
- unless Object.const_defined?(:Mocha)
- require 'mocha'
- require 'stubba'
+unless defined? uses_mocha
+ def uses_mocha(test_name, &block)
+ uses_gem('mocha', test_name, '>= 0.5.5', &block)
end
- yield
-rescue LoadError => load_error
- raise unless load_error.message =~ /mocha/i
- $stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again."
-end \ No newline at end of file
+end
diff --git a/activeresource/test/base_test.rb b/activeresource/test/base_test.rb
index 4addd52636..9a0789e0b4 100644
--- a/activeresource/test/base_test.rb
+++ b/activeresource/test/base_test.rb
@@ -8,7 +8,7 @@ class BaseTest < Test::Unit::TestCase
def setup
@matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
@david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
- @greg = { :id => 3, :name => 'Greg' }.to_xml(:root => 'person')
+ @greg = { :id => 3, :name => 'Greg' }.to_xml(:root => 'person')
@addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address')
@default_request_headers = { 'Content-Type' => 'application/xml' }
@rick = { :name => "Rick", :age => 25 }.to_xml(:root => "person")
@@ -50,7 +50,7 @@ class BaseTest < Test::Unit::TestCase
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/1.xml", {}, @matz
mock.get "/people/2.xml", {}, @david
- mock.get "/people/Greg.xml", {}, @greg
+ mock.get "/people/Greg.xml", {}, @greg
mock.get "/people/4.xml", {'key' => 'value'}, nil, 404
mock.put "/people/1.xml", {}, nil, 204
mock.delete "/people/1.xml", {}, nil, 200
@@ -62,7 +62,7 @@ class BaseTest < Test::Unit::TestCase
mock.get "/people/1/addresses/1.xml", {}, @addy
mock.get "/people/1/addresses/2.xml", {}, nil, 404
mock.get "/people/2/addresses/1.xml", {}, nil, 404
- mock.get "/people/Greg/addresses/1.xml", {}, @addy
+ mock.get "/people/Greg/addresses/1.xml", {}, @addy
mock.put "/people/1/addresses/1.xml", {}, nil, 204
mock.delete "/people/1/addresses/1.xml", {}, nil, 200
mock.post "/people/1/addresses.xml", {}, nil, 201, 'Location' => '/people/1/addresses/5'
@@ -101,13 +101,13 @@ class BaseTest < Test::Unit::TestCase
assert_equal 'http://foo:bar@beast.caboo.se', Forum.site.to_s
assert_equal 'http://foo:bar@beast.caboo.se/forums/:forum_id', Topic.site.to_s
end
-
+
def test_site_variable_can_be_reset
- actor = Class.new(ActiveResource::Base)
+ actor = Class.new(ActiveResource::Base)
assert_nil actor.site
actor.site = 'http://localhost:31337'
actor.site = nil
- assert_nil actor.site
+ assert_nil actor.site
end
def test_should_accept_setting_user
@@ -194,18 +194,18 @@ class BaseTest < Test::Unit::TestCase
actor.site = 'http://nomad'
assert_equal actor.site, jester.site
assert jester.site.frozen?
-
- # Subclasses are always equal to superclass site when not overridden
+
+ # Subclasses are always equal to superclass site when not overridden
fruit = Class.new(ActiveResource::Base)
apple = Class.new(fruit)
-
+
fruit.site = 'http://market'
assert_equal fruit.site, apple.site, 'subclass did not adopt changes from parent class'
-
+
fruit.site = 'http://supermarket'
assert_equal fruit.site, apple.site, 'subclass did not adopt changes from parent class'
end
-
+
def test_user_reader_uses_superclass_user_until_written
# Superclass is Object so returns nil.
assert_nil ActiveResource::Base.user
@@ -317,14 +317,14 @@ class BaseTest < Test::Unit::TestCase
end
def test_updating_baseclass_site_object_wipes_descendent_cached_connection_objects
- # Subclasses are always equal to superclass site when not overridden
+ # Subclasses are always equal to superclass site when not overridden
fruit = Class.new(ActiveResource::Base)
apple = Class.new(fruit)
-
+
fruit.site = 'http://market'
assert_equal fruit.connection.site, apple.connection.site
first_connection = apple.connection.object_id
-
+
fruit.site = 'http://supermarket'
assert_equal fruit.connection.site, apple.connection.site
second_connection = apple.connection.object_id
@@ -393,34 +393,34 @@ class BaseTest < Test::Unit::TestCase
assert_equal '/people.xml?gender=', Person.collection_path(:gender => nil)
assert_equal '/people.xml?gender=male', Person.collection_path('gender' => 'male')
-
+
# Use includes? because ordering of param hash is not guaranteed
assert Person.collection_path(:gender => 'male', :student => true).include?('/people.xml?')
assert Person.collection_path(:gender => 'male', :student => true).include?('gender=male')
assert Person.collection_path(:gender => 'male', :student => true).include?('student=true')
assert_equal '/people.xml?name%5B%5D=bob&name%5B%5D=your+uncle%2Bme&name%5B%5D=&name%5B%5D=false', Person.collection_path(:name => ['bob', 'your uncle+me', nil, false])
-
+
assert_equal '/people.xml?struct%5Ba%5D%5B%5D=2&struct%5Ba%5D%5B%5D=1&struct%5Bb%5D=fred', Person.collection_path(:struct => {:a => [2,1], 'b' => 'fred'})
end
def test_custom_element_path
assert_equal '/people/1/addresses/1.xml', StreetAddress.element_path(1, :person_id => 1)
assert_equal '/people/1/addresses/1.xml', StreetAddress.element_path(1, 'person_id' => 1)
- assert_equal '/people/Greg/addresses/1.xml', StreetAddress.element_path(1, 'person_id' => 'Greg')
+ assert_equal '/people/Greg/addresses/1.xml', StreetAddress.element_path(1, 'person_id' => 'Greg')
end
-
+
def test_custom_element_path_with_redefined_to_param
Person.module_eval do
alias_method :original_to_param_element_path, :to_param
- def to_param
+ def to_param
name
end
end
# Class method.
assert_equal '/people/Greg.xml', Person.element_path('Greg')
-
+
# Protected Instance method.
assert_equal '/people/Greg.xml', Person.find('Greg').send(:element_path)
@@ -470,21 +470,30 @@ class BaseTest < Test::Unit::TestCase
assert_equal "/", Person.prefix
assert_equal Set.new, Person.send!(:prefix_parameters)
end
-
+
def test_set_prefix
SetterTrap.rollback_sets(Person) do |person_class|
person_class.prefix = "the_prefix"
assert_equal "the_prefix", person_class.prefix
end
end
-
+
def test_set_prefix_with_inline_keys
SetterTrap.rollback_sets(Person) do |person_class|
person_class.prefix = "the_prefix:the_param"
assert_equal "the_prefixthe_param_value", person_class.prefix(:the_param => "the_param_value")
end
end
-
+
+ def test_set_prefix_twice_should_clear_params
+ SetterTrap.rollback_sets(Person) do |person_class|
+ person_class.prefix = "the_prefix/:the_param1"
+ assert_equal Set.new([:the_param1]), person_class.prefix_parameters
+ person_class.prefix = "the_prefix/:the_param2"
+ assert_equal Set.new([:the_param2]), person_class.prefix_parameters
+ end
+ end
+
def test_set_prefix_with_default_value
SetterTrap.rollback_sets(Person) do |person_class|
person_class.set_prefix
@@ -504,7 +513,7 @@ class BaseTest < Test::Unit::TestCase
assert_equal "Matz", matz.name
assert matz.name?
end
-
+
def test_respond_to
matz = Person.find(1)
assert matz.respond_to?(:name)
@@ -533,6 +542,12 @@ class BaseTest < Test::Unit::TestCase
assert_equal "Matz", matz.name
end
+ def test_find_last
+ david = Person.find(:last)
+ assert_kind_of Person, david
+ assert_equal 'David', david.name
+ end
+
def test_custom_header
Person.headers['key'] = 'value'
assert_raises(ActiveResource::ResourceNotFound) { Person.find(4) }
@@ -547,7 +562,7 @@ class BaseTest < Test::Unit::TestCase
def test_find_all_by_from
ActiveResource::HttpMock.respond_to { |m| m.get "/companies/1/people.xml", {}, @people_david }
-
+
people = Person.find(:all, :from => "/companies/1/people.xml")
assert_equal 1, people.size
assert_equal "David", people.first.name
@@ -555,7 +570,7 @@ class BaseTest < Test::Unit::TestCase
def test_find_all_by_from_with_options
ActiveResource::HttpMock.respond_to { |m| m.get "/companies/1/people.xml", {}, @people_david }
-
+
people = Person.find(:all, :from => "/companies/1/people.xml")
assert_equal 1, people.size
assert_equal "David", people.first.name
@@ -563,7 +578,7 @@ class BaseTest < Test::Unit::TestCase
def test_find_all_by_symbol_from
ActiveResource::HttpMock.respond_to { |m| m.get "/people/managers.xml", {}, @people_david }
-
+
people = Person.find(:all, :from => :managers)
assert_equal 1, people.size
assert_equal "David", people.first.name
@@ -593,7 +608,7 @@ class BaseTest < Test::Unit::TestCase
p = Person.new
resp = {'Location' => '/foo/bar/1'}
assert_equal '1', p.send!(:id_from_response, resp)
-
+
resp['Location'] << '.xml'
assert_equal '1', p.send!(:id_from_response, resp)
end
@@ -610,16 +625,16 @@ class BaseTest < Test::Unit::TestCase
ryan = Person.new(:id => 1, :name => 'Ryan', :address => address)
assert_equal address.prefix_options, ryan.address.prefix_options
end
-
+
def test_reload_works_with_prefix_options
address = StreetAddress.find(1, :params => { :person_id => 1 })
assert_equal address, address.reload
end
-
+
def test_reload_with_redefined_to_param
Person.module_eval do
alias_method :original_to_param_reload, :to_param
- def to_param
+ def to_param
name
end
end
@@ -634,13 +649,13 @@ class BaseTest < Test::Unit::TestCase
alias_method :reload_to_param, :to_param
alias_method :to_param, :original_to_param_reload
end
- end
-
- def test_reload_works_without_prefix_options
+ end
+
+ def test_reload_works_without_prefix_options
person = Person.find(:first)
assert_equal person, person.reload
end
-
+
def test_create
rick = Person.create(:name => 'Rick')
@@ -650,11 +665,11 @@ class BaseTest < Test::Unit::TestCase
# test additional attribute returned on create
assert_equal 25, rick.age
-
+
# Test that save exceptions get bubbled up too
ActiveResource::HttpMock.respond_to do |mock|
mock.post "/people.xml", {}, nil, 409
- end
+ end
assert_raises(ActiveResource::ResourceConflict) { Person.create(:name => 'Rick') }
end
@@ -716,7 +731,7 @@ class BaseTest < Test::Unit::TestCase
assert_equal "54321 Lane", addy.street
addy.save
end
-
+
def test_update_conflict
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/2.xml", {}, @david
@@ -748,7 +763,7 @@ class BaseTest < Test::Unit::TestCase
end
assert_raises(ActiveResource::ResourceNotFound) { Person.find(1) }
end
-
+
def test_delete_with_custom_prefix
assert StreetAddress.delete(1, :person_id => 1)
ActiveResource::HttpMock.respond_to do |mock|
@@ -778,23 +793,23 @@ class BaseTest < Test::Unit::TestCase
assert !StreetAddress.new({:id => 1, :person_id => 2}).exists?
assert !StreetAddress.new({:id => 2, :person_id => 1}).exists?
end
-
+
def test_exists_with_redefined_to_param
Person.module_eval do
alias_method :original_to_param_exists, :to_param
- def to_param
+ def to_param
name
end
end
# Class method.
- assert Person.exists?('Greg')
+ assert Person.exists?('Greg')
# Instance method.
- assert Person.find('Greg').exists?
+ assert Person.find('Greg').exists?
# Nested class method.
- assert StreetAddress.exists?(1, :params => { :person_id => Person.find('Greg').to_param })
+ assert StreetAddress.exists?(1, :params => { :person_id => Person.find('Greg').to_param })
# Nested instance method.
assert StreetAddress.find(1, :params => { :person_id => Person.find('Greg').to_param }).exists?
@@ -806,8 +821,8 @@ class BaseTest < Test::Unit::TestCase
alias_method :exists_to_param, :to_param
alias_method :to_param, :original_to_param_exists
end
- end
-
+ end
+
def test_to_xml
matz = Person.find(1)
xml = matz.to_xml
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 0526057b15..ae3e2360cd 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -21,8 +21,6 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-$:.unshift(File.dirname(__FILE__))
-
require 'active_support/vendor'
require 'active_support/basic_object'
require 'active_support/inflector'
@@ -57,6 +55,10 @@ require 'active_support/base64'
require 'active_support/time_with_zone'
+I18n.populate do
+ I18n.load_translations File.dirname(__FILE__) + '/active_support/locale/en-US.yml'
+end
+
Inflector = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Inflector', 'ActiveSupport::Inflector')
Dependencies = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Dependencies', 'ActiveSupport::Dependencies')
TimeZone = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('TimeZone', 'ActiveSupport::TimeZone')
diff --git a/activesupport/lib/active_support/basic_object.rb b/activesupport/lib/active_support/basic_object.rb
index e06da79d26..1f77209e7f 100644
--- a/activesupport/lib/active_support/basic_object.rb
+++ b/activesupport/lib/active_support/basic_object.rb
@@ -7,7 +7,7 @@
# barebones base class that emulates Builder::BlankSlate while still relying on
# Ruby 1.9's BasicObject in Ruby 1.9.
module ActiveSupport
- if RUBY_VERSION >= '1.9'
+ if defined? ::BasicObject
class BasicObject < ::BasicObject
undef_method :==
undef_method :equal?
diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb
index 67b0a580ea..6553d72b4f 100644
--- a/activesupport/lib/active_support/buffered_logger.rb
+++ b/activesupport/lib/active_support/buffered_logger.rb
@@ -33,13 +33,12 @@ module ActiveSupport
attr_accessor :level
attr_reader :auto_flushing
- attr_reader :buffer
def initialize(log, level = DEBUG)
@level = level
- @buffer = []
+ @buffer = {}
@auto_flushing = 1
- @no_block = false
+ @guard = Mutex.new
if log.respond_to?(:write)
@log = log
@@ -54,12 +53,6 @@ module ActiveSupport
end
end
- def set_non_blocking_io
- if !RUBY_PLATFORM.match(/java|mswin/) && !(@log == STDOUT) && @log.respond_to?(:write_nonblock)
- @no_block = true
- end
- end
-
def add(severity, message = nil, progname = nil, &block)
return if @level > severity
message = (message || (block && block.call) || progname).to_s
@@ -98,11 +91,11 @@ module ActiveSupport
end
def flush
- unless buffer.empty?
- if @no_block
- @log.write_nonblock(buffer.slice!(0..-1).join)
- else
- @log.write(buffer.slice!(0..-1).join)
+ @guard.synchronize do
+ unless buffer.empty?
+ old_buffer = buffer
+ clear_buffer
+ @log.write(old_buffer.join)
end
end
end
@@ -117,5 +110,13 @@ module ActiveSupport
def auto_flush
flush if buffer.size >= @auto_flushing
end
+
+ def buffer
+ @buffer[Thread.current] ||= []
+ end
+
+ def clear_buffer
+ @buffer[Thread.current] = []
+ end
end
end
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 3e3dc18263..95eae3a77e 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -21,7 +21,7 @@ module ActiveSupport
expanded_cache_key = namespace ? "#{namespace}/" : ""
if ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
- expanded_cache_key << "#{ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]}/"
+ expanded_cache_key << "#{ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]}/"
end
expanded_cache_key << case
@@ -36,16 +36,11 @@ module ActiveSupport
expanded_cache_key
end
-
class Store
cattr_accessor :logger
- def initialize
- end
-
- def threadsafe!
- @mutex = Mutex.new
- self.class.send :include, ThreadSafety
+ def silence!
+ @silence = true
self
end
@@ -110,31 +105,12 @@ module ActiveSupport
nil
end
end
-
+
private
def log(operation, key, options)
- logger.debug("Cache #{operation}: #{key}#{options ? " (#{options.inspect})" : ""}") if logger && !@logger_off
+ logger.debug("Cache #{operation}: #{key}#{options ? " (#{options.inspect})" : ""}") if logger && !@silence && !@logger_off
end
end
-
-
- module ThreadSafety #:nodoc:
- def read(key, options = nil) #:nodoc:
- @mutex.synchronize { super }
- end
-
- def write(key, value, options = nil) #:nodoc:
- @mutex.synchronize { super }
- end
-
- def delete(key, options = nil) #:nodoc:
- @mutex.synchronize { super }
- end
-
- def delete_matched(matcher, options = nil) #:nodoc:
- @mutex.synchronize { super }
- end
- end
end
end
diff --git a/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb b/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb
index 9470ac9f66..0bff6cf9ad 100644
--- a/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb
@@ -1,14 +1,14 @@
module ActiveSupport
module Cache
class CompressedMemCacheStore < MemCacheStore
- def read(name, options = {})
- if value = super(name, options.merge(:raw => true))
+ def read(name, options = nil)
+ if value = super(name, (options || {}).merge(:raw => true))
Marshal.load(ActiveSupport::Gzip.decompress(value))
end
end
- def write(name, value, options = {})
- super(name, ActiveSupport::Gzip.compress(Marshal.dump(value)), options.merge(:raw => true))
+ def write(name, value, options = nil)
+ super(name, ActiveSupport::Gzip.compress(Marshal.dump(value)), (options || {}).merge(:raw => true))
end
end
end
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index 5b771b1da0..659bde64f0 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -9,15 +9,15 @@ module ActiveSupport
def read(name, options = nil)
super
- File.open(real_file_path(name), 'rb') { |f| f.read } rescue nil
+ File.open(real_file_path(name), 'rb') { |f| Marshal.load(f) } rescue nil
end
def write(name, value, options = nil)
super
ensure_cache_path(File.dirname(real_file_path(name)))
- File.open(real_file_path(name), "wb+") { |f| f.write(value) }
+ File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) }
rescue => e
- RAILS_DEFAULT_LOGGER.error "Couldn't create cache directory: #{name} (#{e.message})" if RAILS_DEFAULT_LOGGER
+ logger.error "Couldn't create cache directory: #{name} (#{e.message})" if logger
end
def delete(name, options = nil)
@@ -67,4 +67,4 @@ module ActiveSupport
end
end
end
-end \ No newline at end of file
+end
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index b3769b812f..58958dccef 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -29,8 +29,8 @@ module ActiveSupport
nil
end
- # Set key = value. Pass :unless_exist => true if you don't
- # want to update the cache if the key is already set.
+ # Set key = value. Pass :unless_exist => true if you don't
+ # want to update the cache if the key is already set.
def write(key, value, options = nil)
super
method = options && options[:unless_exist] ? :add : :set
@@ -56,33 +56,33 @@ module ActiveSupport
!read(key, options).nil?
end
- def increment(key, amount = 1)
+ def increment(key, amount = 1)
log("incrementing", key, amount)
-
- response = @data.incr(key, amount)
+
+ response = @data.incr(key, amount)
response == Response::NOT_FOUND ? nil : response
- rescue MemCache::MemCacheError
+ rescue MemCache::MemCacheError
nil
end
def decrement(key, amount = 1)
log("decrement", key, amount)
-
- response = data.decr(key, amount)
+
+ response = @data.decr(key, amount)
response == Response::NOT_FOUND ? nil : response
- rescue MemCache::MemCacheError
+ rescue MemCache::MemCacheError
nil
- end
-
+ end
+
def delete_matched(matcher, options = nil)
super
raise "Not supported by Memcache"
- end
-
+ end
+
def clear
@data.flush_all
- end
-
+ end
+
def stats
@data.stats
end
diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb
index 6f114273e4..f3e4b8c13b 100644
--- a/activesupport/lib/active_support/cache/memory_store.rb
+++ b/activesupport/lib/active_support/cache/memory_store.rb
@@ -3,36 +3,64 @@ module ActiveSupport
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)
- super
- @data[name]
+ @guard.synchronize do
+ super
+ @data[name]
+ end
end
def write(name, value, options = nil)
- super
- @data[name] = value
+ @guard.synchronize do
+ super
+ @data[name] = value.freeze
+ end
end
def delete(name, options = nil)
- super
- @data.delete(name)
+ @guard.synchronize do
+ @data.delete(name)
+ end
end
def delete_matched(matcher, options = nil)
- super
- @data.delete_if { |k,v| k =~ matcher }
+ @guard.synchronize do
+ @data.delete_if { |k,v| k =~ matcher }
+ end
end
def exist?(name,options = nil)
- super
- @data.has_key?(name)
+ @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
end
def clear
- @data.clear
+ @guard.synchronize do
+ @data.clear
+ end
end
end
end
-end \ No newline at end of file
+end
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index 9c59b7ac76..7b905930bb 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -96,15 +96,12 @@ module ActiveSupport
end
end
- def |(chain)
- if chain.is_a?(CallbackChain)
- chain.each { |callback| self | callback }
+ # TODO: Decompose into more Array like behavior
+ def replace_or_append!(chain)
+ if index = index(chain)
+ self[index] = chain
else
- if (found_callback = find(chain)) && (index = index(chain))
- self[index] = chain
- else
- self << chain
- end
+ self << chain
end
self
end
@@ -157,6 +154,14 @@ module ActiveSupport
self.class.new(@kind, @method, @options.dup)
end
+ def hash
+ if @identifier
+ @identifier.hash
+ else
+ @method.hash
+ end
+ end
+
def call(*args, &block)
evaluate_method(method, *args, &block) if should_run_callback?(*args)
rescue LocalJumpError
diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb
index 4ac95efdc7..779ca40aea 100644
--- a/activesupport/lib/active_support/core_ext/array/access.rb
+++ b/activesupport/lib/active_support/core_ext/array/access.rb
@@ -8,6 +8,7 @@ module ActiveSupport #:nodoc:
# %w( a b c d ).from(0) # => %w( a b c d )
# %w( a b c d ).from(2) # => %w( c d )
# %w( a b c d ).from(10) # => nil
+ # %w().from(0) # => nil
def from(position)
self[position..-1]
end
@@ -17,51 +18,52 @@ module ActiveSupport #:nodoc:
# %w( a b c d ).to(0) # => %w( a )
# %w( a b c d ).to(2) # => %w( a b c )
# %w( a b c d ).to(10) # => %w( a b c d )
+ # %w().to(0) # => %w()
def to(position)
self[0..position]
end
- # Equal to self[1]
+ # Equals to <tt>self[1]</tt>.
def second
self[1]
end
- # Equal to self[2]
+ # Equals to <tt>self[2]</tt>.
def third
self[2]
end
- # Equal to self[3]
+ # Equals to <tt>self[3]</tt>.
def fourth
self[3]
end
- # Equal to self[4]
+ # Equals to <tt>self[4]</tt>.
def fifth
self[4]
end
- # Equal to self[5]
+ # Equals to <tt>self[5]</tt>.
def sixth
self[5]
end
- # Equal to self[6]
+ # Equals to <tt>self[6]</tt>.
def seventh
self[6]
end
- # Equal to self[7]
+ # Equals to <tt>self[7]</tt>.
def eighth
self[7]
end
- # Equal to self[8]
+ # Equals to <tt>self[8]</tt>.
def ninth
self[8]
end
- # Equal to self[9]
+ # Equals to <tt>self[9]</tt>.
def tenth
self[9]
end
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
index 49ada8f174..e67b719ddb 100644
--- a/activesupport/lib/active_support/core_ext/array/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -6,10 +6,12 @@ module ActiveSupport #:nodoc:
module Conversions
# Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
# * <tt>:connector</tt> - The word used to join the last element in arrays with two or more elements (default: "and")
- # * <tt>:skip_last_comma</tt> - Set to true to return "a, b and c" instead of "a, b, and c".
- def to_sentence(options = {})
- options.assert_valid_keys(:connector, :skip_last_comma)
- options.reverse_merge! :connector => 'and', :skip_last_comma => false
+ # * <tt>:skip_last_comma</tt> - Set to true to return "a, b and c" instead of "a, b, and c".
+ def to_sentence(options = {})
+ options.assert_valid_keys(:connector, :skip_last_comma, :locale)
+
+ default = I18n.translate(:'support.array.sentence_connector', :locale => options[:locale])
+ options.reverse_merge! :connector => default, :skip_last_comma => false
options[:connector] = "#{options[:connector]} " unless options[:connector].nil? || options[:connector].strip == ''
case length
@@ -23,6 +25,7 @@ module ActiveSupport #:nodoc:
"#{self[0...-1].join(', ')}#{options[:skip_last_comma] ? '' : ','} #{options[:connector]}#{self[-1]}"
end
end
+
# Calls <tt>to_param</tt> on all its elements and joins the result with
# slashes. This is used by <tt>url_for</tt> in Action Pack.
diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb
index df37afb053..dd1484f8fa 100644
--- a/activesupport/lib/active_support/core_ext/array/grouping.rb
+++ b/activesupport/lib/active_support/core_ext/array/grouping.rb
@@ -19,7 +19,7 @@ module ActiveSupport #:nodoc:
# %w(1 2 3).in_groups_of(2, false) {|g| p g}
# ["1", "2"]
# ["3"]
- def in_groups_of(number, fill_with = nil, &block)
+ def in_groups_of(number, fill_with = nil)
if fill_with == false
collection = self
else
@@ -31,7 +31,7 @@ module ActiveSupport #:nodoc:
end
if block_given?
- collection.each_slice(number, &block)
+ collection.each_slice(number) { |slice| yield(slice) }
else
returning [] do |groups|
collection.each_slice(number) { |group| groups << group }
@@ -87,11 +87,11 @@ module ActiveSupport #:nodoc:
#
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
- def split(value = nil, &block)
- block ||= Proc.new { |e| e == value }
+ def split(value = nil)
+ using_block = block_given?
inject([[]]) do |results, element|
- if block.call(element)
+ if (using_block && yield(element)) || (value == element)
results << []
else
results.last << element
diff --git a/activesupport/lib/active_support/core_ext/bigdecimal/conversions.rb b/activesupport/lib/active_support/core_ext/bigdecimal/conversions.rb
index d2b01b1b8d..94c7c779f7 100644
--- a/activesupport/lib/active_support/core_ext/bigdecimal/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/bigdecimal/conversions.rb
@@ -21,7 +21,7 @@ module ActiveSupport #:nodoc:
# 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 reconsituting floats
+ # 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
diff --git a/activesupport/lib/active_support/core_ext/date/behavior.rb b/activesupport/lib/active_support/core_ext/date/behavior.rb
index 011cc17cbf..6f8f7c6a82 100644
--- a/activesupport/lib/active_support/core_ext/date/behavior.rb
+++ b/activesupport/lib/active_support/core_ext/date/behavior.rb
@@ -7,6 +7,22 @@ module ActiveSupport #:nodoc:
def acts_like_date?
true
end
+
+ # Date memoizes some instance methods using metaprogramming to wrap
+ # the methods with one that caches the result in an instance variable.
+ # If a Date is frozen but the memoized method hasn't been called, the
+ # first call will result in a frozen object error since the memo
+ # instance variable is uninitialized. Work around by eagerly memoizing
+ # before freezing.
+ def freeze #:nodoc:
+ self.class.private_instance_methods(false).each do |m|
+ if m.to_s =~ /\A__\d+__\Z/
+ instance_variable_set(:"@#{m}", [send(m)])
+ end
+ end
+
+ super
+ end
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/duplicable.rb b/activesupport/lib/active_support/core_ext/duplicable.rb
index adbbfd8c60..8f49ddfd9e 100644
--- a/activesupport/lib/active_support/core_ext/duplicable.rb
+++ b/activesupport/lib/active_support/core_ext/duplicable.rb
@@ -35,3 +35,9 @@ class Numeric #:nodoc:
false
end
end
+
+class Class #:nodoc:
+ def duplicable?
+ false
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/file.rb b/activesupport/lib/active_support/core_ext/file.rb
index 45d93b220f..e03f8ac44e 100644
--- a/activesupport/lib/active_support/core_ext/file.rb
+++ b/activesupport/lib/active_support/core_ext/file.rb
@@ -1,21 +1,5 @@
-require 'tempfile'
+require 'active_support/core_ext/file/atomic'
-# Write to a file atomically. Useful for situations where you don't
-# want other processes or threads to see half-written files.
-#
-# File.atomic_write("important.file") do |file|
-# file.write("hello")
-# end
-#
-# If your temp directory is not on the same filesystem as the file you're
-# trying to write, you can provide a different temporary directory.
-#
-# File.atomic_write("/data/something.important", "/data/tmp") do |f|
-# file.write("hello")
-# end
-def File.atomic_write(file_name, temp_dir = Dir.tmpdir)
- temp_file = Tempfile.new(File.basename(file_name), temp_dir)
- yield temp_file
- temp_file.close
- File.rename(temp_file.path, file_name)
-end \ No newline at end of file
+class File #:nodoc:
+ extend ActiveSupport::CoreExtensions::File::Atomic
+end
diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb
new file mode 100644
index 0000000000..4d3cf5423f
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/file/atomic.rb
@@ -0,0 +1,46 @@
+require 'tempfile'
+
+module ActiveSupport #:nodoc:
+ module CoreExtensions #:nodoc:
+ module File #:nodoc:
+ module Atomic
+ # Write to a file atomically. Useful for situations where you don't
+ # want other processes or threads to see half-written files.
+ #
+ # File.atomic_write("important.file") do |file|
+ # file.write("hello")
+ # end
+ #
+ # If your temp directory is not on the same filesystem as the file you're
+ # trying to write, you can provide a different temporary directory.
+ #
+ # File.atomic_write("/data/something.important", "/data/tmp") do |f|
+ # file.write("hello")
+ # end
+ def atomic_write(file_name, temp_dir = Dir.tmpdir)
+ temp_file = Tempfile.new(basename(file_name), temp_dir)
+ yield temp_file
+ temp_file.close
+
+ begin
+ # Get original file permissions
+ old_stat = stat(file_name)
+ 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")
+ old_stat = stat(check_name)
+ unlink(check_name)
+ end
+
+ # Overwrite original file with temp file
+ rename(temp_file.path, file_name)
+
+ # Set correct permissions on new file
+ chown(old_stat.uid, old_stat.gid, file_name)
+ chmod(old_stat.mode, file_name)
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb
index 6cbd9dd378..a6065ab48e 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -1,10 +1,11 @@
-%w(keys indifferent_access reverse_merge conversions diff slice except).each do |ext|
+%w(keys indifferent_access deep_merge reverse_merge conversions diff slice except).each do |ext|
require "active_support/core_ext/hash/#{ext}"
end
class Hash #:nodoc:
include ActiveSupport::CoreExtensions::Hash::Keys
include ActiveSupport::CoreExtensions::Hash::IndifferentAccess
+ include ActiveSupport::CoreExtensions::Hash::DeepMerge
include ActiveSupport::CoreExtensions::Hash::ReverseMerge
include ActiveSupport::CoreExtensions::Hash::Conversions
include ActiveSupport::CoreExtensions::Hash::Diff
diff --git a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
new file mode 100644
index 0000000000..f8842ba57a
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
@@ -0,0 +1,23 @@
+module ActiveSupport #:nodoc:
+ module CoreExtensions #:nodoc:
+ module Hash #:nodoc:
+ # Allows for deep merging
+ module DeepMerge
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
+ def deep_merge(other_hash)
+ self.merge(other_hash) do |key, oldval, newval|
+ oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
+ newval = newval.to_hash if newval.respond_to?(:to_hash)
+ oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' ? oldval.deep_merge(newval) : newval
+ end
+ end
+
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
+ # Modifies the receiver in place.
+ def deep_merge!(other_hash)
+ replace(deep_merge(other_hash))
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb
index bc97fa35a6..f26d01553d 100644
--- a/activesupport/lib/active_support/core_ext/hash/except.rb
+++ b/activesupport/lib/active_support/core_ext/hash/except.rb
@@ -13,7 +13,7 @@ module ActiveSupport #:nodoc:
clone.except!(*keys)
end
- # Replaces the hash without only the given keys.
+ # Replaces the hash without the given keys.
def except!(*keys)
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
keys.each { |key| delete(key) }
diff --git a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb
index 7af10846e7..546e261cc9 100644
--- a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb
+++ b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb
@@ -1,21 +1,28 @@
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module Hash #:nodoc:
- # Allows for reverse merging where its the keys in the calling hash that wins over those in the <tt>other_hash</tt>.
- # This is particularly useful for initializing an incoming option hash with default values:
+ # Allows for reverse merging two hashes where the keys in the calling hash take precedence over those
+ # in the <tt>other_hash</tt>. This is particularly useful for initializing an option hash with default values:
#
# def setup(options = {})
# options.reverse_merge! :size => 25, :velocity => 10
# end
#
- # The default <tt>:size</tt> and <tt>:velocity</tt> is only set if the +options+ passed in doesn't already have those keys set.
+ # Using <tt>merge</tt>, the above example would look as follows:
+ #
+ # def setup(options = {})
+ # { :size => 25, :velocity => 10 }.merge(options)
+ # end
+ #
+ # The default <tt>:size</tt> and <tt>:velocity</tt> are only set if the +options+ hash passed in doesn't already
+ # have the respective key.
module ReverseMerge
- # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
+ # Performs the opposite of <tt>merge</tt>, with the keys and values from the first hash taking precedence over the second.
def reverse_merge(other_hash)
other_hash.merge(self)
end
- # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
+ # Performs the opposite of <tt>merge</tt>, with the keys and values from the first hash taking precedence over the second.
# Modifies the receiver in place.
def reverse_merge!(other_hash)
replace(reverse_merge(other_hash))
diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb
index be4dec6e53..3f14470f36 100644
--- a/activesupport/lib/active_support/core_ext/hash/slice.rb
+++ b/activesupport/lib/active_support/core_ext/hash/slice.rb
@@ -9,6 +9,11 @@ module ActiveSupport #:nodoc:
# end
#
# search(options.slice(:mass, :velocity, :time))
+ #
+ # If you have an array of keys you want to limit to, you should splat them:
+ #
+ # valid_keys = [:mass, :velocity, :time]
+ # search(options.slice(*valid_keys))
module Slice
# Returns a new hash with only the given keys.
def slice(*keys)
diff --git a/activesupport/lib/active_support/core_ext/kernel/debugger.rb b/activesupport/lib/active_support/core_ext/kernel/debugger.rb
index c74d6cf884..4007a647be 100644
--- a/activesupport/lib/active_support/core_ext/kernel/debugger.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/debugger.rb
@@ -2,12 +2,12 @@ module Kernel
unless respond_to?(:debugger)
# Starts a debugging session if ruby-debug has been loaded (call script/server --debugger to do load it).
def debugger
- RAILS_DEFAULT_LOGGER.info "\n***** Debugger requested, but was not available: Start server with --debugger to enable *****\n"
+ Rails.logger.info "\n***** Debugger requested, but was not available: Start server with --debugger to enable *****\n"
end
end
-
+
def breakpoint
- RAILS_DEFAULT_LOGGER.info "\n***** The 'breakpoint' command has been renamed 'debugger' -- please change *****\n"
+ Rails.logger.info "\n***** The 'breakpoint' command has been renamed 'debugger' -- please change *****\n"
debugger
end
-end \ No newline at end of file
+end
diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb
index bbc7d81672..0796a7b710 100644
--- a/activesupport/lib/active_support/core_ext/object.rb
+++ b/activesupport/lib/active_support/core_ext/object.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/object/conversions'
require 'active_support/core_ext/object/extending'
require 'active_support/core_ext/object/instance_variables'
+require 'active_support/core_ext/object/metaclass'
require 'active_support/core_ext/object/misc'
diff --git a/activesupport/lib/active_support/core_ext/object/metaclass.rb b/activesupport/lib/active_support/core_ext/object/metaclass.rb
new file mode 100644
index 0000000000..93fb0ad594
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/object/metaclass.rb
@@ -0,0 +1,13 @@
+class Object
+ # Get object's meta (ghost, eigenclass, singleton) class
+ def metaclass
+ class << self
+ self
+ end
+ end
+
+ # If class_eval is called on an object, add those methods to its metaclass
+ def class_eval(*args, &block)
+ metaclass.class_eval(*args, &block)
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index a009d7c085..3bbad7dad8 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -24,8 +24,8 @@ module ActiveSupport #:nodoc:
#
# "posts".singularize # => "post"
# "octopi".singularize # => "octopus"
- # "sheep".singluarize # => "sheep"
- # "word".singluarize # => "word"
+ # "sheep".singularize # => "sheep"
+ # "word".singularize # => "word"
# "the blue mailmen".singularize # => "the blue mailman"
# "CamelOctopi".singularize # => "CamelOctopus"
def singularize
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index 2cce782676..cd234c9b89 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -261,7 +261,7 @@ module ActiveSupport #:nodoc:
# Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances
# can be chronologically compared with a Time
def compare_with_coercion(other)
- # if other is an ActiveSupport::TimeWithZone, coerce a Time instance from it so we can do <=> comparision
+ # if other is an ActiveSupport::TimeWithZone, coerce a Time instance from it so we can do <=> comparison
other = other.comparable_time if other.respond_to?(:comparable_time)
if other.acts_like?(:date)
# other is a Date/DateTime, so coerce self #to_datetime and hand off to DateTime#<=>
diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb
index 9054008309..f42be46770 100644
--- a/activesupport/lib/active_support/core_ext/time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/time/conversions.rb
@@ -30,6 +30,7 @@ module ActiveSupport #:nodoc:
# time.to_s(:time) # => "06:10:17"
#
# time.to_formatted_s(:db) # => "2007-01-18 06:10:17"
+ # time.to_formatted_s(:number) # => "20070118061017"
# time.to_formatted_s(:short) # => "18 Jan 06:10"
# time.to_formatted_s(:long) # => "January 18, 2007 06:10"
# time.to_formatted_s(:long_ordinal) # => "January 18th, 2007 06:10"
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index 2f3fa72bb4..3d871eec11 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -1,8 +1,3 @@
-require 'set'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/load_error'
-require 'active_support/core_ext/kernel'
-
module ActiveSupport #:nodoc:
module Dependencies #:nodoc:
extend self
@@ -44,6 +39,10 @@ module ActiveSupport #:nodoc:
mattr_accessor :explicitly_unloadable_constants
self.explicitly_unloadable_constants = []
+ # The logger is used for generating information on the action run-time (including benchmarking) if available.
+ # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
+ mattr_accessor :logger
+
# Set to true to enable logging of const_missing and file loads
mattr_accessor :log_activity
self.log_activity = false
@@ -52,6 +51,159 @@ module ActiveSupport #:nodoc:
mattr_accessor :constant_watch_stack
self.constant_watch_stack = []
+ # Module includes this module
+ module ModuleConstMissing #:nodoc:
+ def self.included(base) #:nodoc:
+ base.class_eval do
+ unless defined? const_missing_without_dependencies
+ alias_method_chain :const_missing, :dependencies
+ end
+ end
+ end
+
+ def self.excluded(base) #:nodoc:
+ base.class_eval do
+ if defined? const_missing_without_dependencies
+ undef_method :const_missing
+ alias_method :const_missing, :const_missing_without_dependencies
+ undef_method :const_missing_without_dependencies
+ end
+ end
+ end
+
+ # Use const_missing to autoload associations so we don't have to
+ # require_association when using single-table inheritance.
+ def const_missing_with_dependencies(class_id)
+ ActiveSupport::Dependencies.load_missing_constant self, class_id
+ end
+
+ def unloadable(const_desc = self)
+ super(const_desc)
+ end
+ end
+
+ # Class includes this module
+ module ClassConstMissing #:nodoc:
+ def const_missing(const_name)
+ if [Object, Kernel].include?(self) || parent == self
+ super
+ else
+ begin
+ begin
+ Dependencies.load_missing_constant self, const_name
+ rescue NameError
+ parent.send :const_missing, const_name
+ end
+ rescue NameError => e
+ # Make sure that the name we are missing is the one that caused the error
+ parent_qualified_name = Dependencies.qualified_name_for parent, const_name
+ raise unless e.missing_name? parent_qualified_name
+ qualified_name = Dependencies.qualified_name_for self, const_name
+ raise NameError.new("uninitialized constant #{qualified_name}").copy_blame!(e)
+ end
+ end
+ end
+ end
+
+ # Object includes this module
+ module Loadable #:nodoc:
+ def self.included(base) #:nodoc:
+ base.class_eval do
+ unless defined? load_without_new_constant_marking
+ alias_method_chain :load, :new_constant_marking
+ end
+ end
+ end
+
+ def self.excluded(base) #:nodoc:
+ base.class_eval do
+ if defined? load_without_new_constant_marking
+ undef_method :load
+ alias_method :load, :load_without_new_constant_marking
+ undef_method :load_without_new_constant_marking
+ end
+ end
+ end
+
+ def require_or_load(file_name)
+ Dependencies.require_or_load(file_name)
+ end
+
+ def require_dependency(file_name)
+ Dependencies.depend_on(file_name)
+ end
+
+ def require_association(file_name)
+ Dependencies.associate_with(file_name)
+ end
+
+ def load_with_new_constant_marking(file, *extras) #:nodoc:
+ Dependencies.new_constants_in(Object) { load_without_new_constant_marking(file, *extras) }
+ rescue Exception => exception # errors from loading file
+ exception.blame_file! file
+ raise
+ end
+
+ def require(file, *extras) #:nodoc:
+ Dependencies.new_constants_in(Object) { super }
+ rescue Exception => exception # errors from required file
+ exception.blame_file! file
+ raise
+ end
+
+ # Mark the given constant as unloadable. Unloadable constants are removed each
+ # time dependencies are cleared.
+ #
+ # Note that marking a constant for unloading need only be done once. Setup
+ # or init scripts may list each unloadable constant that may need unloading;
+ # each constant will be removed for every subsequent clear, as opposed to for
+ # the first clear.
+ #
+ # The provided constant descriptor may be a (non-anonymous) module or class,
+ # or a qualified constant name as a string or symbol.
+ #
+ # Returns true if the constant was not previously marked for unloading, false
+ # otherwise.
+ def unloadable(const_desc)
+ Dependencies.mark_for_unload const_desc
+ end
+ end
+
+ # Exception file-blaming
+ module Blamable #:nodoc:
+ def blame_file!(file)
+ (@blamed_files ||= []).unshift file
+ end
+
+ def blamed_files
+ @blamed_files ||= []
+ end
+
+ def describe_blame
+ return nil if blamed_files.empty?
+ "This error occurred while loading the following files:\n #{blamed_files.join "\n "}"
+ end
+
+ def copy_blame!(exc)
+ @blamed_files = exc.blamed_files.clone
+ self
+ end
+ end
+
+ def hook!
+ Object.instance_eval { include Loadable }
+ Module.instance_eval { include ModuleConstMissing }
+ Class.instance_eval { include ClassConstMissing }
+ Exception.instance_eval { include Blamable }
+ true
+ end
+
+ def unhook!
+ ModuleConstMissing.excluded(Module)
+ Loadable.excluded(Object)
+ true
+ end
+
def load?
mechanism == :load
end
@@ -436,7 +588,7 @@ module ActiveSupport #:nodoc:
protected
def log_call(*args)
- if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER && log_activity
+ if logger && log_activity
arg_str = args.collect { |arg| arg.inspect } * ', '
/in `([a-z_\?\!]+)'/ =~ caller(1).first
selector = $1 || '<unknown>'
@@ -445,109 +597,11 @@ module ActiveSupport #:nodoc:
end
def log(msg)
- if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER && log_activity
- RAILS_DEFAULT_LOGGER.debug "Dependencies: #{msg}"
+ if logger && log_activity
+ logger.debug "Dependencies: #{msg}"
end
end
end
end
-Object.instance_eval do
- define_method(:require_or_load) { |file_name| ActiveSupport::Dependencies.require_or_load(file_name) } unless Object.respond_to?(:require_or_load)
- define_method(:require_dependency) { |file_name| ActiveSupport::Dependencies.depend_on(file_name) } unless Object.respond_to?(:require_dependency)
- define_method(:require_association) { |file_name| ActiveSupport::Dependencies.associate_with(file_name) } unless Object.respond_to?(:require_association)
-end
-
-class Module #:nodoc:
- # Rename the original handler so we can chain it to the new one
- alias :rails_original_const_missing :const_missing
-
- # Use const_missing to autoload associations so we don't have to
- # require_association when using single-table inheritance.
- def const_missing(class_id)
- ActiveSupport::Dependencies.load_missing_constant self, class_id
- end
-
- def unloadable(const_desc = self)
- super(const_desc)
- end
-
-end
-
-class Class
- def const_missing(const_name)
- if [Object, Kernel].include?(self) || parent == self
- super
- else
- begin
- begin
- ActiveSupport::Dependencies.load_missing_constant self, const_name
- rescue NameError
- parent.send :const_missing, const_name
- end
- rescue NameError => e
- # Make sure that the name we are missing is the one that caused the error
- parent_qualified_name = ActiveSupport::Dependencies.qualified_name_for parent, const_name
- raise unless e.missing_name? parent_qualified_name
- qualified_name = ActiveSupport::Dependencies.qualified_name_for self, const_name
- raise NameError.new("uninitialized constant #{qualified_name}").copy_blame!(e)
- end
- end
- end
-end
-
-class Object
- alias_method :load_without_new_constant_marking, :load
-
- def load(file, *extras) #:nodoc:
- ActiveSupport::Dependencies.new_constants_in(Object) { super }
- rescue Exception => exception # errors from loading file
- exception.blame_file! file
- raise
- end
-
- def require(file, *extras) #:nodoc:
- ActiveSupport::Dependencies.new_constants_in(Object) { super }
- rescue Exception => exception # errors from required file
- exception.blame_file! file
- raise
- end
-
- # Mark the given constant as unloadable. Unloadable constants are removed each
- # time dependencies are cleared.
- #
- # Note that marking a constant for unloading need only be done once. Setup
- # or init scripts may list each unloadable constant that may need unloading;
- # each constant will be removed for every subsequent clear, as opposed to for
- # the first clear.
- #
- # The provided constant descriptor may be a (non-anonymous) module or class,
- # or a qualified constant name as a string or symbol.
- #
- # Returns true if the constant was not previously marked for unloading, false
- # otherwise.
- def unloadable(const_desc)
- ActiveSupport::Dependencies.mark_for_unload const_desc
- end
-end
-
-# Add file-blaming to exceptions
-class Exception #:nodoc:
- def blame_file!(file)
- (@blamed_files ||= []).unshift file
- end
-
- def blamed_files
- @blamed_files ||= []
- end
-
- def describe_blame
- return nil if blamed_files.empty?
- "This error occurred while loading the following files:\n #{blamed_files.join "\n "}"
- end
-
- def copy_blame!(exc)
- @blamed_files = exc.blamed_files.clone
- self
- end
-end
+ActiveSupport::Dependencies.hook!
diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb
index ebdaf86146..01eb5df593 100644
--- a/activesupport/lib/active_support/deprecation.rb
+++ b/activesupport/lib/active_support/deprecation.rb
@@ -185,6 +185,10 @@ module ActiveSupport
@new_const = new_const
end
+ def class
+ target.class
+ end
+
private
def target
@new_const.to_s.constantize
diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb
index 6651569d33..7ae9e0c6ab 100644
--- a/activesupport/lib/active_support/inflector.rb
+++ b/activesupport/lib/active_support/inflector.rb
@@ -39,12 +39,16 @@ module ActiveSupport
# Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression.
# The replacement should always be a string that may include references to the matched data from the rule.
def plural(rule, replacement)
+ @uncountables.delete(rule) if rule.is_a?(String)
+ @uncountables.delete(replacement)
@plurals.insert(0, [rule, replacement])
end
# Specifies a new singularization rule and its replacement. The rule can either be a string or a regular expression.
# The replacement should always be a string that may include references to the matched data from the rule.
def singular(rule, replacement)
+ @uncountables.delete(rule) if rule.is_a?(String)
+ @uncountables.delete(replacement)
@singulars.insert(0, [rule, replacement])
end
@@ -55,6 +59,8 @@ module ActiveSupport
# irregular 'octopus', 'octopi'
# irregular 'person', 'people'
def irregular(singular, plural)
+ @uncountables.delete(singular)
+ @uncountables.delete(plural)
if singular[0,1].upcase == plural[0,1].upcase
plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1])
singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1])
@@ -173,7 +179,7 @@ module ActiveSupport
if first_letter_in_uppercase
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
else
- lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1]
+ lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
end
end
@@ -273,29 +279,47 @@ module ActiveSupport
underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
end
- # Tries to find a constant with the name specified in the argument string:
- #
- # "Module".constantize # => Module
- # "Test::Unit".constantize # => Test::Unit
- #
- # The name is assumed to be the one of a top-level constant, no matter whether
- # it starts with "::" or not. No lexical context is taken into account:
- #
- # C = 'outside'
- # module M
- # C = 'inside'
- # C # => 'inside'
- # "C".constantize # => 'outside', same as ::C
- # end
- #
- # NameError is raised when the name is not in CamelCase or the constant is
- # unknown.
- def constantize(camel_cased_word)
- unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word
- raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!"
+ # Ruby 1.9 introduces an inherit argument for Module#const_get and
+ # #const_defined? and changes their default behavior.
+ if Module.method(:const_get).arity == 1
+ # Tries to find a constant with the name specified in the argument string:
+ #
+ # "Module".constantize # => Module
+ # "Test::Unit".constantize # => Test::Unit
+ #
+ # The name is assumed to be the one of a top-level constant, no matter whether
+ # it starts with "::" or not. No lexical context is taken into account:
+ #
+ # C = 'outside'
+ # module M
+ # C = 'inside'
+ # C # => 'inside'
+ # "C".constantize # => 'outside', same as ::C
+ # end
+ #
+ # NameError is raised when the name is not in CamelCase or the constant is
+ # unknown.
+ def constantize(camel_cased_word)
+ names = camel_cased_word.split('::')
+ names.shift if names.empty? || names.first.empty?
+
+ constant = Object
+ names.each do |name|
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
+ end
+ constant
end
+ else
+ def constantize(camel_cased_word) #:nodoc:
+ names = camel_cased_word.split('::')
+ names.shift if names.empty? || names.first.empty?
- Object.module_eval("::#{$1}", __FILE__, __LINE__)
+ constant = Object
+ names.each do |name|
+ constant = constant.const_get(name, false) || constant.const_missing(name)
+ end
+ constant
+ end
end
# Turns a number into an ordinal string used to denote the position in an
@@ -326,4 +350,4 @@ require 'active_support/inflections'
require 'active_support/core_ext/string/inflections'
unless String.included_modules.include?(ActiveSupport::CoreExtensions::String::Inflections)
String.send :include, ActiveSupport::CoreExtensions::String::Inflections
-end \ No newline at end of file
+end
diff --git a/activesupport/lib/active_support/json.rb b/activesupport/lib/active_support/json.rb
index 54a7becd0f..2bdb4a7b11 100644
--- a/activesupport/lib/active_support/json.rb
+++ b/activesupport/lib/active_support/json.rb
@@ -1,5 +1,5 @@
module ActiveSupport
- # If true, use ISO 8601 format for dates and times. Otherwise, fall back to the Active Support legacy format.
+ # If true, use ISO 8601 format for dates and times. Otherwise, fall back to the Active Support legacy format.
mattr_accessor :use_standard_json_time_format
class << self
diff --git a/activesupport/lib/active_support/json/encoders/date.rb b/activesupport/lib/active_support/json/encoders/date.rb
index cb9419d29d..1fc99c466f 100644
--- a/activesupport/lib/active_support/json/encoders/date.rb
+++ b/activesupport/lib/active_support/json/encoders/date.rb
@@ -1,7 +1,14 @@
class Date
- # Returns a JSON string representing the date.
+ # Returns a JSON string representing the date. If ActiveSupport.use_standard_json_time_format is set to true, the
+ # ISO 8601 format is used.
#
- # ==== Example:
+ # ==== Examples:
+ #
+ # # With ActiveSupport.use_standard_json_time_format = true
+ # Date.new(2005,2,1).to_json
+ # # => "2005-02-01"
+ #
+ # # With ActiveSupport.use_standard_json_time_format = false
# Date.new(2005,2,1).to_json
# # => "2005/02/01"
def to_json(options = nil)
diff --git a/activesupport/lib/active_support/json/encoders/date_time.rb b/activesupport/lib/active_support/json/encoders/date_time.rb
index a4a5efbfb1..e259930033 100644
--- a/activesupport/lib/active_support/json/encoders/date_time.rb
+++ b/activesupport/lib/active_support/json/encoders/date_time.rb
@@ -1,7 +1,14 @@
class DateTime
- # Returns a JSON string representing the datetime.
+ # Returns a JSON string representing the datetime. If ActiveSupport.use_standard_json_time_format is set to true, the
+ # ISO 8601 format is used.
#
- # ==== Example:
+ # ==== Examples:
+ #
+ # # With ActiveSupport.use_standard_json_time_format = true
+ # DateTime.civil(2005,2,1,15,15,10).to_json
+ # # => "2005-02-01T15:15:10+00:00"
+ #
+ # # With ActiveSupport.use_standard_json_time_format = false
# DateTime.civil(2005,2,1,15,15,10).to_json
# # => "2005/02/01 15:15:10 +0000"
def to_json(options = nil)
diff --git a/activesupport/lib/active_support/json/encoders/time.rb b/activesupport/lib/active_support/json/encoders/time.rb
index 57ed3c9e31..09fc614889 100644
--- a/activesupport/lib/active_support/json/encoders/time.rb
+++ b/activesupport/lib/active_support/json/encoders/time.rb
@@ -1,9 +1,16 @@
class Time
- # Returns a JSON string representing the time.
+ # Returns a JSON string representing the time. If ActiveSupport.use_standard_json_time_format is set to true, the
+ # ISO 8601 format is used.
#
- # ==== Example:
+ # ==== Examples:
+ #
+ # # With ActiveSupport.use_standard_json_time_format = true
+ # Time.utc(2005,2,1,15,15,10).to_json
+ # # => "2005-02-01T15:15:10Z"
+ #
+ # # With ActiveSupport.use_standard_json_time_format = false
# Time.utc(2005,2,1,15,15,10).to_json
- # # => 2005/02/01 15:15:10 +0000"
+ # # => "2005/02/01 15:15:10 +0000"
def to_json(options = nil)
if ActiveSupport.use_standard_json_time_format
xmlschema.inspect
diff --git a/activesupport/lib/active_support/locale/en-US.yml b/activesupport/lib/active_support/locale/en-US.yml
new file mode 100644
index 0000000000..60ecb1d42a
--- /dev/null
+++ b/activesupport/lib/active_support/locale/en-US.yml
@@ -0,0 +1,31 @@
+en-US:
+ date:
+ formats:
+ # Use the strftime parameters for formats.
+ # When no format has been given, it uses default.
+ # You can provide other formats here if you like!
+ default: "%Y-%m-%d"
+ short: "%b %d"
+ long: "%B %d, %Y"
+
+ day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
+ abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
+
+ # Don't forget the nil at the beginning; there's no such thing as a 0th month
+ month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
+ abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
+ # Used in date_select and datime_select.
+ order: [ :year, :month, :day ]
+
+ time:
+ formats:
+ default: "%a, %d %b %Y %H:%M:%S %z"
+ short: "%d %b %H:%M"
+ long: "%B %d, %Y %H:%M"
+ am: "am"
+ pm: "pm"
+
+# Used in array.to_sentence.
+ support:
+ array:
+ sentence_connector: "and"
diff --git a/activesupport/lib/active_support/memoizable.rb b/activesupport/lib/active_support/memoizable.rb
index d06250171a..6506238ac0 100644
--- a/activesupport/lib/active_support/memoizable.rb
+++ b/activesupport/lib/active_support/memoizable.rb
@@ -1,35 +1,78 @@
module ActiveSupport
module Memoizable
- def self.included(base) #:nodoc:
- base.extend(ClassMethods)
- end
+ module Freezable
+ def self.included(base)
+ base.class_eval do
+ unless base.method_defined?(:freeze_without_memoizable)
+ alias_method_chain :freeze, :memoizable
+ end
+ end
+ end
- module ClassMethods
- def memoize(symbol)
- original_method = "_unmemoized_#{symbol}"
- memoized_ivar = "@_memoized_#{symbol}"
- raise "Already memoized #{symbol}" if instance_methods.map(&:to_s).include?(original_method)
+ def freeze_with_memoizable
+ memoize_all unless frozen?
+ freeze_without_memoizable
+ end
- alias_method original_method, symbol
- class_eval <<-EOS, __FILE__, __LINE__
- def #{symbol}
- if defined? #{memoized_ivar}
- #{memoized_ivar}
+ def memoize_all
+ methods.each do |m|
+ if m.to_s =~ /^_unmemoized_(.*)/
+ if method(m).arity == 0
+ __send__($1)
else
- #{memoized_ivar} = #{original_method}
+ ivar = :"@_memoized_#{$1}"
+ instance_variable_set(ivar, {})
end
end
- EOS
+ end
end
- end
- def freeze
- methods.each do |method|
- if m = method.to_s.match(/\A_unmemoized_(.*)/)
- send(m[1]).freeze
+ def unmemoize_all
+ methods.each do |m|
+ if m.to_s =~ /^_unmemoized_(.*)/
+ ivar = :"@_memoized_#{$1}"
+ instance_variable_get(ivar).clear if instance_variable_defined?(ivar)
+ end
end
end
- super
+ end
+
+ def memoize(*symbols)
+ symbols.each do |symbol|
+ original_method = :"_unmemoized_#{symbol}"
+ memoized_ivar = :"@_memoized_#{symbol.to_s.sub(/\?\Z/, '_query').sub(/!\Z/, '_bang')}"
+
+ class_eval <<-EOS, __FILE__, __LINE__
+ include Freezable
+
+ raise "Already memoized #{symbol}" if method_defined?(:#{original_method})
+ alias #{original_method} #{symbol}
+
+ if instance_method(:#{symbol}).arity == 0
+ def #{symbol}(reload = false)
+ if reload || !defined?(#{memoized_ivar}) || #{memoized_ivar}.empty?
+ #{memoized_ivar} = [#{original_method}.freeze]
+ end
+ #{memoized_ivar}[0]
+ end
+ else
+ def #{symbol}(*args)
+ #{memoized_ivar} ||= {} unless frozen?
+ reload = args.pop if args.last == true || args.last == :reload
+
+ if #{memoized_ivar}
+ if !reload && #{memoized_ivar}.has_key?(args)
+ #{memoized_ivar}[args]
+ elsif #{memoized_ivar}
+ #{memoized_ivar}[args] = #{original_method}(*args).freeze
+ end
+ else
+ #{original_method}(*args)
+ end
+ end
+ end
+ EOS
+ end
end
end
end
diff --git a/activesupport/lib/active_support/multibyte/generators/generate_tables.rb b/activesupport/lib/active_support/multibyte/generators/generate_tables.rb
index 7f807585c5..7f807585c5 100644..100755
--- a/activesupport/lib/active_support/multibyte/generators/generate_tables.rb
+++ b/activesupport/lib/active_support/multibyte/generators/generate_tables.rb
diff --git a/activesupport/lib/active_support/option_merger.rb b/activesupport/lib/active_support/option_merger.rb
index 1a4ff9db9a..c77bca1ac9 100644
--- a/activesupport/lib/active_support/option_merger.rb
+++ b/activesupport/lib/active_support/option_merger.rb
@@ -10,16 +10,8 @@ module ActiveSupport
private
def method_missing(method, *arguments, &block)
- merge_argument_options! arguments
+ arguments << (arguments.last.respond_to?(:to_hash) ? @options.deep_merge(arguments.pop) : @options.dup)
@context.send!(method, *arguments, &block)
end
-
- def merge_argument_options!(arguments)
- arguments << if arguments.last.respond_to? :to_hash
- @options.merge(arguments.pop)
- else
- @options.dup
- end
- end
end
end
diff --git a/activesupport/lib/active_support/testing/performance.rb b/activesupport/lib/active_support/testing/performance.rb
index 5f2027eb3b..f996c40793 100644
--- a/activesupport/lib/active_support/testing/performance.rb
+++ b/activesupport/lib/active_support/testing/performance.rb
@@ -11,21 +11,21 @@ module ActiveSupport
DEFAULTS =
if benchmark = ARGV.include?('--benchmark') # HAX for rake test
{ :benchmark => true,
- :runs => 10,
+ :runs => 4,
:metrics => [:process_time, :memory, :objects, :gc_runs, :gc_time],
:output => 'tmp/performance' }
else
{ :benchmark => false,
:runs => 1,
- :min_percent => 0.02,
+ :min_percent => 0.01,
:metrics => [:process_time, :memory, :objects],
:formats => [:flat, :graph_html, :call_tree],
:output => 'tmp/performance' }
- end
+ end.freeze
def self.included(base)
- base.class_inheritable_hash :profile_options
- base.profile_options = DEFAULTS.dup
+ base.superclass_delegating_accessor :profile_options
+ base.profile_options = DEFAULTS
end
def full_test_name
@@ -39,12 +39,12 @@ module ActiveSupport
@_result = result
run_warmup
- profile_options[:metrics].each do |metric_name|
- if klass = Metrics[metric_name.to_sym]
- run_profile(klass.new)
- result.add_run
- else
- $stderr.puts '%20s: unsupported' % metric_name.to_s
+ if profile_options && metrics = profile_options[:metrics]
+ metrics.each do |metric_name|
+ if klass = Metrics[metric_name.to_sym]
+ run_profile(klass.new)
+ result.add_run
+ end
end
end
@@ -72,13 +72,13 @@ module ActiveSupport
protected
def run_warmup
- 5.times { GC.start }
+ GC.start
time = Metrics::Time.new
run_test(time, :benchmark)
puts "%s (%s warmup)" % [full_test_name, time.format(time.total)]
- 5.times { GC.start }
+ GC.start
end
def run_profile(metric)
@@ -164,7 +164,14 @@ module ActiveSupport
end
class Profiler < Performer
+ def initialize(*args)
+ super
+ @supported = @metric.measure_mode rescue false
+ end
+
def run
+ return unless @supported
+
RubyProf.measure_mode = @metric.measure_mode
RubyProf.start
RubyProf.pause
@@ -173,7 +180,17 @@ module ActiveSupport
@total = @data.threads.values.sum(0) { |method_infos| method_infos.sort.last.total_time }
end
+ def report
+ if @supported
+ super
+ else
+ '%20s: unsupported' % @metric.name
+ end
+ end
+
def record
+ return unless @supported
+
klasses = profile_options[:formats].map { |f| RubyProf.const_get("#{f.to_s.camelize}Printer") }.compact
klasses.each do |klass|
@@ -202,8 +219,7 @@ module ActiveSupport
module Metrics
def self.[](name)
- klass = const_get(name.to_s.camelize)
- klass if klass::Mode
+ const_get(name.to_s.camelize)
rescue NameError
nil
end
@@ -250,6 +266,16 @@ module ActiveSupport
ensure
GC.disable_stats
end
+ elsif defined?(GC::Profiler)
+ def with_gc_stats
+ GC.start
+ GC.disable
+ GC::Profiler.enable
+ yield
+ ensure
+ GC::Profiler.disable
+ GC.enable
+ end
else
def with_gc_stats
yield
@@ -310,7 +336,7 @@ module ActiveSupport
RubyProf.measure_memory / 1024.0
end
- # Ruby 1.8 + adymo patch
+ # Ruby 1.8 + railsbench patch
elsif GC.respond_to?(:allocated_size)
def measure
GC.allocated_size / 1024.0
@@ -322,11 +348,27 @@ module ActiveSupport
GC.heap_info['heap_current_memory'] / 1024.0
end
+ # Ruby 1.9 with total_malloc_allocated_size patch
+ elsif GC.respond_to?(:malloc_total_allocated_size)
+ def measure
+ GC.total_malloc_allocated_size / 1024.0
+ end
+
# Ruby 1.9 unpatched
elsif GC.respond_to?(:malloc_allocated_size)
def measure
GC.malloc_allocated_size / 1024.0
end
+
+ # Ruby 1.9 + GC profiler patch
+ elsif defined?(GC::Profiler)
+ def measure
+ GC.enable
+ GC.start
+ kb = GC::Profiler.data.last[:HEAP_USE_SIZE] / 1024.0
+ GC.disable
+ kb
+ end
end
def format(measurement)
@@ -341,10 +383,23 @@ module ActiveSupport
def measure
RubyProf.measure_allocations
end
+
+ # Ruby 1.8 + railsbench patch
elsif ObjectSpace.respond_to?(:allocated_objects)
def measure
ObjectSpace.allocated_objects
end
+
+ # Ruby 1.9 + GC profiler patch
+ elsif defined?(GC::Profiler)
+ def measure
+ GC.enable
+ GC.start
+ last = GC::Profiler.data.last
+ count = last[:HEAP_LIVE_OBJECTS] + last[:HEAP_FREE_OBJECTS]
+ GC.disable
+ count
+ end
end
def format(measurement)
diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb
index 21d71eb92a..a514b61fea 100644
--- a/activesupport/lib/active_support/testing/setup_and_teardown.rb
+++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb
@@ -15,12 +15,15 @@ module ActiveSupport
define_callbacks :setup, :teardown
if defined?(::Mini)
+ undef_method :run
alias_method :run, :run_with_callbacks_and_miniunit
else
begin
require 'mocha'
+ undef_method :run
alias_method :run, :run_with_callbacks_and_mocha
rescue LoadError
+ undef_method :run
alias_method :run, :run_with_callbacks_and_testunit
end
end
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index e85bfe9b2e..4866fa0dc8 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -102,7 +102,19 @@ module ActiveSupport
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{formatted_offset(true, 'Z')}"
end
alias_method :iso8601, :xmlschema
-
+
+ # Returns a JSON string representing the TimeWithZone. If ActiveSupport.use_standard_json_time_format is set to
+ # true, the ISO 8601 format is used.
+ #
+ # ==== Examples:
+ #
+ # # With ActiveSupport.use_standard_json_time_format = true
+ # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
+ # # => "2005-02-01T15:15:10Z"
+ #
+ # # With ActiveSupport.use_standard_json_time_format = false
+ # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
+ # # => "2005/02/01 15:15:10 +0000"
def to_json(options = nil)
if ActiveSupport.use_standard_json_time_format
xmlschema.inspect
diff --git a/activesupport/lib/active_support/vendor.rb b/activesupport/lib/active_support/vendor.rb
index a02e42f791..acd94af783 100644
--- a/activesupport/lib/active_support/vendor.rb
+++ b/activesupport/lib/active_support/vendor.rb
@@ -23,4 +23,12 @@ begin
gem 'tzinfo', '~> 0.3.9'
rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/vendor/tzinfo-0.3.9"
-end \ No newline at end of file
+end
+
+# TODO I18n gem has not been released yet
+# begin
+# gem 'i18n', '~> 0.0.1'
+# rescue Gem::LoadError
+ $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.0.1"
+ require 'i18n'
+# end \ No newline at end of file
diff --git a/activesupport/lib/active_support/vendor/builder-2.1.2/builder/xmlevents.rb b/activesupport/lib/active_support/vendor/builder-2.1.2/builder/xmlevents.rb
index 91fcd21e13..b373e4da3c 100644
--- a/activesupport/lib/active_support/vendor/builder-2.1.2/builder/xmlevents.rb
+++ b/activesupport/lib/active_support/vendor/builder-2.1.2/builder/xmlevents.rb
@@ -20,7 +20,7 @@ module Builder
# markup.
#
# Usage:
- # xe = Builder::XmlEvents.new(hander)
+ # xe = Builder::XmlEvents.new(handler)
# xe.title("HI") # Sends start_tag/end_tag/text messages to the handler.
#
# Indentation may also be selected by providing value for the
diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb
new file mode 100755
index 0000000000..9e347d94e9
--- /dev/null
+++ b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb
@@ -0,0 +1,192 @@
+# Authors:: Matt Aimonetti (http://railsontherun.com/),
+# Sven Fuchs (http://www.artweb-design.de),
+# Joshua Harvey (http://www.workingwithrails.com/person/759-joshua-harvey),
+# Saimon Moore (http://saimonmoore.net),
+# Stephan Soller (http://www.arkanis-development.de/)
+# Copyright:: Copyright (c) 2008 The Ruby i18n Team
+# License:: MIT
+require 'i18n/backend/simple'
+require 'i18n/exceptions'
+
+module I18n
+ @@backend = nil
+ @@default_locale = 'en-US'
+ @@exception_handler = :default_exception_handler
+
+ class << self
+ # Returns the current backend. Defaults to +Backend::Simple+.
+ def backend
+ @@backend ||= Backend::Simple.new
+ end
+
+ # Sets the current backend. Used to set a custom backend.
+ def backend=(backend)
+ @@backend = backend
+ end
+
+ # Returns the current default locale. Defaults to 'en-US'
+ def default_locale
+ @@default_locale
+ end
+
+ # Sets the current default locale. Used to set a custom default locale.
+ def default_locale=(locale)
+ @@default_locale = locale
+ end
+
+ # Returns the current locale. Defaults to I18n.default_locale.
+ def locale
+ Thread.current[:locale] ||= default_locale
+ end
+
+ # Sets the current locale pseudo-globally, i.e. in the Thread.current hash.
+ def locale=(locale)
+ Thread.current[:locale] = locale
+ end
+
+ # Sets the exception handler.
+ def exception_handler=(exception_handler)
+ @@exception_handler = exception_handler
+ end
+
+ # Allow client libraries to pass a block that populates the translation
+ # storage. Decoupled for backends like a db backend that persist their
+ # translations, so the backend can decide whether/when to yield or not.
+ def populate(&block)
+ backend.populate(&block)
+ end
+
+ # Allows client libraries to pass arguments that specify a source for
+ # translation data to be loaded by the backend. The backend defines
+ # acceptable sources.
+ # E.g. the provided SimpleBackend accepts a list of paths to translation
+ # files which are either named *.rb and contain plain Ruby Hashes or are
+ # named *.yml and contain YAML data.)
+ def load_translations(*args)
+ backend.load_translations(*args)
+ end
+
+ # Stores translations for the given locale in the backend.
+ def store_translations(locale, data)
+ backend.store_translations locale, data
+ end
+
+ # Translates, pluralizes and interpolates a given key using a given locale,
+ # scope, and default, as well as interpolation values.
+ #
+ # *LOOKUP*
+ #
+ # Translation data is organized as a nested hash using the upper-level keys
+ # as namespaces. <em>E.g.</em>, ActionView ships with the translation:
+ # <tt>:date => {:formats => {:short => "%b %d"}}</tt>.
+ #
+ # Translations can be looked up at any level of this hash using the key argument
+ # and the scope option. <em>E.g.</em>, in this example <tt>I18n.t :date</tt>
+ # returns the whole translations hash <tt>{:formats => {:short => "%b %d"}}</tt>.
+ #
+ # Key can be either a single key or a dot-separated key (both Strings and Symbols
+ # work). <em>E.g.</em>, the short format can be looked up using both:
+ # I18n.t 'date.formats.short'
+ # I18n.t :'date.formats.short'
+ #
+ # Scope can be either a single key, a dot-separated key or an array of keys
+ # or dot-separated keys. Keys and scopes can be combined freely. So these
+ # examples will all look up the same short date format:
+ # I18n.t 'date.formats.short'
+ # I18n.t 'formats.short', :scope => 'date'
+ # I18n.t 'short', :scope => 'date.formats'
+ # I18n.t 'short', :scope => %w(date formats)
+ #
+ # *INTERPOLATION*
+ #
+ # Translations can contain interpolation variables which will be replaced by
+ # values passed to #translate as part of the options hash, with the keys matching
+ # the interpolation variable names.
+ #
+ # <em>E.g.</em>, with a translation <tt>:foo => "foo {{bar}}"</tt> the option
+ # value for the key +bar+ will be interpolated into the translation:
+ # I18n.t :foo, :bar => 'baz' # => 'foo baz'
+ #
+ # *PLURALIZATION*
+ #
+ # Translation data can contain pluralized translations. Pluralized translations
+ # are arrays of singluar/plural versions of translations like <tt>['Foo', 'Foos']</tt>.
+ #
+ # Note that <tt>I18n::Backend::Simple</tt> only supports an algorithm for English
+ # pluralization rules. Other algorithms can be supported by custom backends.
+ #
+ # This returns the singular version of a pluralized translation:
+ # I18n.t :foo, :count => 1 # => 'Foo'
+ #
+ # These both return the plural version of a pluralized translation:
+ # I18n.t :foo, :count => 0 # => 'Foos'
+ # I18n.t :foo, :count => 2 # => 'Foos'
+ #
+ # The <tt>:count</tt> option can be used both for pluralization and interpolation.
+ # <em>E.g.</em>, with the translation
+ # <tt>:foo => ['{{count}} foo', '{{count}} foos']</tt>, count will
+ # be interpolated to the pluralized translation:
+ # I18n.t :foo, :count => 1 # => '1 foo'
+ #
+ # *DEFAULTS*
+ #
+ # This returns the translation for <tt>:foo</tt> or <tt>default</tt> if no translation was found:
+ # I18n.t :foo, :default => 'default'
+ #
+ # This returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt> if no
+ # translation for <tt>:foo</tt> was found:
+ # I18n.t :foo, :default => :bar
+ #
+ # Returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt>
+ # or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found.
+ # I18n.t :foo, :default => [:bar, 'default']
+ #
+ # <b>BULK LOOKUP</b>
+ #
+ # This returns an array with the translations for <tt>:foo</tt> and <tt>:bar</tt>.
+ # I18n.t [:foo, :bar]
+ #
+ # Can be used with dot-separated nested keys:
+ # I18n.t [:'baz.foo', :'baz.bar']
+ #
+ # Which is the same as using a scope option:
+ # I18n.t [:foo, :bar], :scope => :baz
+ def translate(key, options = {})
+ locale = options.delete(:locale) || I18n.locale
+ backend.translate locale, key, options
+ rescue I18n::ArgumentError => e
+ raise e if options[:raise]
+ send @@exception_handler, e, locale, key, options
+ end
+ alias :t :translate
+
+ # Localizes certain objects, such as dates and numbers to local formatting.
+ def localize(object, options = {})
+ locale = options[:locale] || I18n.locale
+ format = options[:format] || :default
+ backend.localize(locale, object, format)
+ end
+ alias :l :localize
+
+ protected
+ # Handles exceptions raised in the backend. All exceptions except for
+ # MissingTranslationData exceptions are re-raised. When a MissingTranslationData
+ # was caught and the option :raise is not set the handler returns an error
+ # message string containing the key/scope.
+ def default_exception_handler(exception, locale, key, options)
+ return exception.message if MissingTranslationData === exception
+ raise exception
+ end
+
+ # Merges the given locale, key and scope into a single array of keys.
+ # Splits keys that contain dots into multiple keys. Makes sure all
+ # keys are Symbols.
+ def normalize_translation_keys(locale, key, scope)
+ keys = [locale] + Array(scope) + [key]
+ keys = keys.map{|k| k.to_s.split(/\./) }
+ keys.flatten.map{|k| k.to_sym}
+ end
+ end
+end
+
+
diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb
new file mode 100644
index 0000000000..a53f7fe772
--- /dev/null
+++ b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb
@@ -0,0 +1,193 @@
+require 'strscan'
+
+module I18n
+ module Backend
+ class Simple
+ # Allow client libraries to pass a block that populates the translation
+ # storage. Decoupled for backends like a db backend that persist their
+ # translations, so the backend can decide whether/when to yield or not.
+ def populate(&block)
+ yield
+ end
+
+ # Accepts a list of paths to translation files. Loads translations from
+ # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
+ # for details.
+ def load_translations(*filenames)
+ filenames.each {|filename| load_file filename }
+ end
+
+ # Stores translations for the given locale in memory.
+ # This uses a deep merge for the translations hash, so existing
+ # translations will be overwritten by new ones only at the deepest
+ # level of the hash.
+ def store_translations(locale, data)
+ merge_translations(locale, data)
+ end
+
+ def translate(locale, key, options = {})
+ raise InvalidLocale.new(locale) if locale.nil?
+ return key.map{|k| translate locale, k, options } if key.is_a? Array
+
+ reserved = :scope, :default
+ count, scope, default = options.values_at(:count, *reserved)
+ options.delete(:default)
+ values = options.reject{|name, value| reserved.include? name }
+
+ entry = lookup(locale, key, scope) || default(locale, default, options) || raise(I18n::MissingTranslationData.new(locale, key, options))
+ entry = pluralize locale, entry, count
+ entry = interpolate locale, entry, values
+ entry
+ end
+
+ # Acts the same as +strftime+, but returns a localized version of the
+ # formatted date string. Takes a key from the date/time formats
+ # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
+ def localize(locale, object, format = :default)
+ raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
+
+ type = object.respond_to?(:sec) ? 'time' : 'date'
+ formats = translate(locale, :"#{type}.formats")
+ format = formats[format.to_sym] if formats && formats[format.to_sym]
+ # TODO raise exception unless format found?
+ format = format.to_s.dup
+
+ format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
+ format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
+ format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
+ format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
+ format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
+ object.strftime(format)
+ end
+
+ protected
+
+ def translations
+ @translations ||= {}
+ end
+
+ # Looks up a translation from the translations hash. Returns nil if
+ # eiher key is nil, or locale, scope or key do not exist as a key in the
+ # nested translations hash. Splits keys or scopes containing dots
+ # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
+ # <tt>%w(currency format)</tt>.
+ def lookup(locale, key, scope = [])
+ return unless key
+ keys = I18n.send :normalize_translation_keys, locale, key, scope
+ keys.inject(translations){|result, k| result[k.to_sym] or return nil }
+ end
+
+ # Evaluates a default translation.
+ # If the given default is a String it is used literally. If it is a Symbol
+ # it will be translated with the given options. If it is an Array the first
+ # translation yielded will be returned.
+ #
+ # <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
+ # <tt>translate(locale, :foo)</tt> does not yield a result.
+ def default(locale, default, options = {})
+ case default
+ when String then default
+ when Symbol then translate locale, default, options
+ when Array then default.each do |obj|
+ result = default(locale, obj, options.dup) and return result
+ end and nil
+ end
+ rescue MissingTranslationData
+ nil
+ end
+
+ # Picks a translation from an array according to English pluralization
+ # rules. It will pick the first translation if count is not equal to 1
+ # and the second translation if it is equal to 1. Other backends can
+ # implement more flexible or complex pluralization rules.
+ def pluralize(locale, entry, count)
+ return entry unless entry.is_a?(Hash) and count
+ # raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
+ key = :zero if count == 0 && entry.has_key?(:zero)
+ key ||= count == 1 ? :one : :other
+ raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
+ entry[key]
+ end
+
+ # Interpolates values into a given string.
+ #
+ # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
+ # # => "file test.txt opened by {{user}}"
+ #
+ # Note that you have to double escape the <tt>\\</tt> when you want to escape
+ # the <tt>{{...}}</tt> key in a string (once for the string and once for the
+ # interpolation).
+ def interpolate(locale, string, values = {})
+ return string if !string.is_a?(String)
+
+ string = string.gsub(/%d/, '{{count}}').gsub(/%s/, '{{value}}')
+ if string.respond_to?(:force_encoding)
+ original_encoding = string.encoding
+ string.force_encoding(Encoding::BINARY)
+ end
+ s = StringScanner.new(string)
+
+ while s.skip_until(/\{\{/)
+ s.string[s.pos - 3, 1] = '' and next if s.pre_match[-1, 1] == '\\'
+ start_pos = s.pos - 2
+ key = s.scan_until(/\}\}/)[0..-3]
+ end_pos = s.pos - 1
+
+ raise ReservedInterpolationKey.new(key, string) if %w(scope default).include?(key)
+ raise MissingInterpolationArgument.new(key, string) unless values.has_key? key.to_sym
+
+ s.string[start_pos..end_pos] = values[key.to_sym].to_s
+ s.unscan
+ end
+
+ result = s.string
+ result.force_encoding(original_encoding) if original_encoding
+ result
+ end
+
+ # Loads a single translations file by delegating to #load_rb or
+ # #load_yml depending on the file extension and directly merges the
+ # data to the existing translations. Raises I18n::UnknownFileType
+ # for all other file extensions.
+ def load_file(filename)
+ type = File.extname(filename).tr('.', '').downcase
+ raise UnknownFileType.new(type, filename) unless respond_to? :"load_#{type}"
+ data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
+ data.each{|locale, d| merge_translations locale, d }
+ end
+
+ # Loads a plain Ruby translations file. eval'ing the file must yield
+ # a Hash containing translation data with locales as toplevel keys.
+ def load_rb(filename)
+ eval IO.read(filename), binding, filename
+ end
+
+ # Loads a YAML translations file. The data must have locales as
+ # toplevel keys.
+ def load_yml(filename)
+ YAML::load IO.read(filename)
+ end
+
+ # Deep merges the given translations hash with the existing translations
+ # for the given locale
+ def merge_translations(locale, data)
+ locale = locale.to_sym
+ translations[locale] ||= {}
+ data = deep_symbolize_keys data
+
+ # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
+ merger = proc{|key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
+ translations[locale].merge! data, &merger
+ end
+
+ # Return a new hash with all keys and nested keys converted to symbols.
+ def deep_symbolize_keys(hash)
+ hash.inject({}){|result, (key, value)|
+ value = deep_symbolize_keys(value) if value.is_a? Hash
+ result[(key.to_sym rescue key) || key] = value
+ result
+ }
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/exceptions.rb b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/exceptions.rb
new file mode 100644
index 0000000000..0f3eff1071
--- /dev/null
+++ b/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/exceptions.rb
@@ -0,0 +1,53 @@
+module I18n
+ class ArgumentError < ::ArgumentError; end
+
+ class InvalidLocale < ArgumentError
+ attr_reader :locale
+ def initialize(locale)
+ @locale = locale
+ super "#{locale.inspect} is not a valid locale"
+ end
+ end
+
+ class MissingTranslationData < ArgumentError
+ attr_reader :locale, :key, :options
+ def initialize(locale, key, options)
+ @key, @locale, @options = key, locale, options
+ keys = I18n.send(:normalize_translation_keys, locale, key, options[:scope])
+ keys << 'no key' if keys.size < 2
+ super "translation missing: #{keys.join(', ')}"
+ end
+ end
+
+ class InvalidPluralizationData < ArgumentError
+ attr_reader :entry, :count
+ def initialize(entry, count)
+ @entry, @count = entry, count
+ super "translation data #{entry.inspect} can not be used with :count => #{count}"
+ end
+ end
+
+ class MissingInterpolationArgument < ArgumentError
+ attr_reader :key, :string
+ def initialize(key, string)
+ @key, @string = key, string
+ super "interpolation argument #{key} missing in #{string.inspect}"
+ end
+ end
+
+ class ReservedInterpolationKey < ArgumentError
+ attr_reader :key, :string
+ def initialize(key, string)
+ @key, @string = key, string
+ super "reserved key #{key.inspect} used in #{string.inspect}"
+ end
+ end
+
+ class UnknownFileType < ArgumentError
+ attr_reader :type, :filename
+ def initialize(type, filename)
+ @type, @filename = type, filename
+ super "can not load translations from #{filename}, the file type #{type} is not known"
+ end
+ end
+end \ No newline at end of file
diff --git a/activesupport/lib/active_support/vendor/memcache-client-1.5.0/memcache.rb b/activesupport/lib/active_support/vendor/memcache-client-1.5.0/memcache.rb
index dda7f2c30e..30113201a6 100644
--- a/activesupport/lib/active_support/vendor/memcache-client-1.5.0/memcache.rb
+++ b/activesupport/lib/active_support/vendor/memcache-client-1.5.0/memcache.rb
@@ -119,7 +119,7 @@ class MemCache
# Valid options for +opts+ are:
#
# [:namespace] Prepends this value to all keys added or retrieved.
- # [:readonly] Raises an exeception on cache writes when true.
+ # [:readonly] Raises an exception on cache writes when true.
# [:multithread] Wraps cache access in a Mutex for thread safety.
#
# Other options are ignored.
@@ -207,7 +207,7 @@ class MemCache
end
##
- # Deceremets the value for +key+ by +amount+ and returns the new value.
+ # Decrements the value for +key+ by +amount+ and returns the new value.
# +key+ must already exist. If +key+ is not an integer, it is assumed to be
# 0. +key+ can not be decremented below 0.
diff --git a/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/data_timezone.rb b/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/data_timezone.rb
index 5eccbdf0db..2510e90b5b 100644
--- a/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/data_timezone.rb
+++ b/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/data_timezone.rb
@@ -38,7 +38,7 @@ module TZInfo
# Returns the set of TimezonePeriod instances that are valid for the given
# local time as an array. If you just want a single period, use
- # period_for_local instead and specify how abiguities should be resolved.
+ # period_for_local instead and specify how ambiguities should be resolved.
# Raises PeriodNotFound if no periods are found for the given time.
def periods_for_local(local)
info.periods_for_local(local)
diff --git a/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/data_timezone_info.rb b/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/data_timezone_info.rb
index a45d94554b..8829ba9cc8 100644
--- a/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/data_timezone_info.rb
+++ b/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/data_timezone_info.rb
@@ -66,7 +66,7 @@ module TZInfo
# ArgumentError will be raised if a transition is added out of order.
# offset_id refers to an id defined with offset. ArgumentError will be
# raised if the offset_id cannot be found. numerator_or_time and
- # denomiator specify the time the transition occurs as. See
+ # denominator specify the time the transition occurs as. See
# TimezoneTransitionInfo for more detail about specifying times.
def transition(year, month, offset_id, numerator_or_time, denominator = nil)
offset = @offsets[offset_id]
diff --git a/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/linked_timezone.rb b/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/linked_timezone.rb
index f8ec4fca87..c757485b84 100644
--- a/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/linked_timezone.rb
+++ b/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/linked_timezone.rb
@@ -36,7 +36,7 @@ module TZInfo
# Returns the set of TimezonePeriod instances that are valid for the given
# local time as an array. If you just want a single period, use
- # period_for_local instead and specify how abiguities should be resolved.
+ # period_for_local instead and specify how ambiguities should be resolved.
# Raises PeriodNotFound if no periods are found for the given time.
def periods_for_local(local)
@linked_timezone.periods_for_local(local)
diff --git a/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/timezone.rb b/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/timezone.rb
index f87fb6fb70..eeaa772d0f 100644
--- a/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/timezone.rb
+++ b/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/timezone.rb
@@ -121,7 +121,7 @@ module TZInfo
TimezoneProxy.new(identifier)
end
- # If identifier is nil calls super(), otherwise calls get. An identfier
+ # If identifier is nil calls super(), otherwise calls get. An identifier
# should always be passed in when called externally.
def self.new(identifier = nil)
if identifier
diff --git a/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/timezone_transition_info.rb b/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/timezone_transition_info.rb
index 3fecb24f0d..781764f0b0 100644
--- a/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/timezone_transition_info.rb
+++ b/activesupport/lib/active_support/vendor/tzinfo-0.3.9/tzinfo/timezone_transition_info.rb
@@ -37,7 +37,7 @@ module TZInfo
attr_reader :numerator_or_time
protected :numerator_or_time
- # Either the denominotor of the DateTime if the transition time is defined
+ # Either the denominator of the DateTime if the transition time is defined
# as a DateTime, otherwise nil.
attr_reader :denominator
protected :denominator
diff --git a/activesupport/lib/active_support/vendor/xml-simple-1.0.11/xmlsimple.rb b/activesupport/lib/active_support/vendor/xml-simple-1.0.11/xmlsimple.rb
index 0de24c0eff..ec6dab513f 100644
--- a/activesupport/lib/active_support/vendor/xml-simple-1.0.11/xmlsimple.rb
+++ b/activesupport/lib/active_support/vendor/xml-simple-1.0.11/xmlsimple.rb
@@ -121,7 +121,7 @@ class XmlSimple
# Create a "global" cache.
@@cache = Cache.new
- # Creates and intializes a new XmlSimple object.
+ # Creates and initializes a new XmlSimple object.
#
# defaults::
# Default values for options.
@@ -497,7 +497,7 @@ class XmlSimple
}
end
- # Fold Hases containing a single anonymous Array up into just the Array.
+ # Fold Hashes containing a single anonymous Array up into just the Array.
if count == 1
anonymoustag = @options['anonymoustag']
if result.has_key?(anonymoustag) && result[anonymoustag].instance_of?(Array)
@@ -907,7 +907,7 @@ class XmlSimple
# Thanks to Norbert Gawor for a bugfix.
#
# value::
- # Value to be checked for emptyness.
+ # Value to be checked for emptiness.
def empty(value)
case value
when Hash
diff --git a/activesupport/test/buffered_logger_test.rb b/activesupport/test/buffered_logger_test.rb
index 97649518b7..6319c09210 100644
--- a/activesupport/test/buffered_logger_test.rb
+++ b/activesupport/test/buffered_logger_test.rb
@@ -70,7 +70,7 @@ class BufferedLoggerTest < Test::Unit::TestCase
end
@logger.flush
- assert !@output.string.empty?, @logger.buffer.size
+ assert !@output.string.empty?, @logger.send(:buffer).size
end
define_method "test_disabling_auto_flush_with_#{disable.inspect}_should_flush_at_max_buffer_size_as_failsafe" do
@@ -83,10 +83,10 @@ class BufferedLoggerTest < Test::Unit::TestCase
end
@logger.info 'there it is.'
- assert !@output.string.empty?, @logger.buffer.size
+ assert !@output.string.empty?, @logger.send(:buffer).size
end
end
-
+
def test_should_know_if_its_loglevel_is_below_a_given_level
ActiveSupport::BufferedLogger::Severity.constants.each do |level|
@logger.level = ActiveSupport::BufferedLogger::Severity.const_get(level) - 1
@@ -105,7 +105,7 @@ class BufferedLoggerTest < Test::Unit::TestCase
@logger.info 'there it is.'
assert !@output.string.empty?, @output.string
end
-
+
def test_should_create_the_log_directory_if_it_doesnt_exist
tmp_directory = File.join(File.dirname(__FILE__), "tmp")
log_file = File.join(tmp_directory, "development.log")
@@ -115,4 +115,25 @@ class BufferedLoggerTest < Test::Unit::TestCase
ensure
FileUtils.rm_rf(tmp_directory)
end
+
+ def test_logger_should_maintain_separate_buffers_for_each_thread
+ @logger.auto_flushing = false
+
+ a = Thread.new do
+ @logger.info("a"); Thread.pass;
+ @logger.info("b"); Thread.pass;
+ @logger.info("c"); @logger.flush
+ end
+
+ b = Thread.new do
+ @logger.info("x"); Thread.pass;
+ @logger.info("y"); Thread.pass;
+ @logger.info("z"); @logger.flush
+ end
+
+ a.join
+ b.join
+
+ assert_equal "a\nb\nc\nx\ny\nz\n", @output.string
+ end
end
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index f3220d27aa..9ea9389448 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -70,3 +70,60 @@ uses_mocha 'high-level cache store tests' do
end
end
end
+
+class FileStoreTest < Test::Unit::TestCase
+ def setup
+ @cache = ActiveSupport::Cache.lookup_store(:file_store, Dir.pwd)
+ end
+
+ def test_should_read_and_write_strings
+ @cache.write('foo', 'bar')
+ assert_equal 'bar', @cache.read('foo')
+ ensure
+ File.delete("foo.cache")
+ end
+
+ def test_should_read_and_write_hash
+ @cache.write('foo', {:a => "b"})
+ assert_equal({:a => "b"}, @cache.read('foo'))
+ ensure
+ File.delete("foo.cache")
+ end
+
+ def test_should_read_and_write_nil
+ @cache.write('foo', nil)
+ assert_equal nil, @cache.read('foo')
+ ensure
+ File.delete("foo.cache")
+ end
+end
+
+class MemoryStoreTest < Test::Unit::TestCase
+ def setup
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store)
+ end
+
+ def test_should_read_and_write
+ @cache.write('foo', 'bar')
+ assert_equal 'bar', @cache.read('foo')
+ end
+
+ def test_fetch_without_cache_miss
+ @cache.write('foo', 'bar')
+ assert_equal 'bar', @cache.fetch('foo') { 'baz' }
+ end
+
+ def test_fetch_with_cache_miss
+ assert_equal 'baz', @cache.fetch('foo') { 'baz' }
+ end
+
+ def test_fetch_with_forced_cache_miss
+ @cache.fetch('foo', :force => true) { 'bar' }
+ end
+
+ def test_store_objects_should_be_immutable
+ @cache.write('foo', 'bar')
+ assert_raise(ActiveSupport::FrozenObjectError) { @cache.read('foo').gsub!(/.*/, 'baz') }
+ assert_equal 'bar', @cache.read('foo')
+ end
+end
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index 7f71ca2262..25b8eecef5 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -134,10 +134,10 @@ class CallbackChainTest < Test::Unit::TestCase
assert_equal :bacon, @chain.find(:bacon).method
end
- def test_union
- assert_equal [:bacon, :lettuce, :tomato], (@chain | Callback.new(:make, :bacon)).map(&:method)
- assert_equal [:bacon, :lettuce, :tomato, :turkey], (@chain | CallbackChain.build(:make, :bacon, :lettuce, :tomato, :turkey)).map(&:method)
- assert_equal [:bacon, :lettuce, :tomato, :turkey, :mayo], (@chain | Callback.new(:make, :mayo)).map(&:method)
+ def test_replace_or_append
+ assert_equal [:bacon, :lettuce, :tomato], (@chain.replace_or_append!(Callback.new(:make, :bacon))).map(&:method)
+ assert_equal [:bacon, :lettuce, :tomato, :turkey], (@chain.replace_or_append!(Callback.new(:make, :turkey))).map(&:method)
+ assert_equal [:bacon, :lettuce, :tomato, :turkey, :mayo], (@chain.replace_or_append!(Callback.new(:make, :mayo))).map(&:method)
end
def test_delete
diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb
index ddfe1f904f..0f3cf4c75c 100644
--- a/activesupport/test/core_ext/date_ext_test.rb
+++ b/activesupport/test/core_ext/date_ext_test.rb
@@ -198,10 +198,6 @@ class DateExtCalculationsTest < Test::Unit::TestCase
assert_equal Time.local(2005,2,21,23,59,59), Date.new(2005,2,21).end_of_day
end
- def test_date_acts_like_date
- assert Date.new.acts_like_date?
- end
-
def test_xmlschema
with_env_tz 'US/Eastern' do
assert_match(/^1980-02-28T00:00:00-05:?00$/, Date.new(1980, 2, 28).xmlschema)
@@ -245,3 +241,15 @@ class DateExtCalculationsTest < Test::Unit::TestCase
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
end
+
+class DateExtBehaviorTest < Test::Unit::TestCase
+ def test_date_acts_like_date
+ assert Date.new.acts_like_date?
+ end
+
+ def test_freeze_doesnt_clobber_memoized_instance_methods
+ assert_nothing_raised do
+ Date.today.freeze.inspect
+ end
+ end
+end
diff --git a/activesupport/test/core_ext/duplicable_test.rb b/activesupport/test/core_ext/duplicable_test.rb
index 3ccfedccd7..8b6127f31e 100644
--- a/activesupport/test/core_ext/duplicable_test.rb
+++ b/activesupport/test/core_ext/duplicable_test.rb
@@ -1,7 +1,7 @@
require 'abstract_unit'
class DuplicableTest < Test::Unit::TestCase
- NO = [nil, false, true, :symbol, 1, 2.3, BigDecimal.new('4.56')]
+ NO = [nil, false, true, :symbol, 1, 2.3, BigDecimal.new('4.56'), Class.new]
YES = ['1', Object.new, /foo/, [], {}, Time.now]
def test_duplicable
diff --git a/activesupport/test/core_ext/file_test.rb b/activesupport/test/core_ext/file_test.rb
index 5efe357e9f..eedc6b592b 100644
--- a/activesupport/test/core_ext/file_test.rb
+++ b/activesupport/test/core_ext/file_test.rb
@@ -1,9 +1,8 @@
require 'abstract_unit'
class AtomicWriteTest < Test::Unit::TestCase
-
def test_atomic_write_without_errors
- contents = "Atomic Text"
+ contents = "Atomic Text"
File.atomic_write(file_name, Dir.pwd) do |file|
file.write(contents)
assert !File.exist?(file_name)
@@ -13,7 +12,7 @@ class AtomicWriteTest < Test::Unit::TestCase
ensure
File.unlink(file_name) rescue nil
end
-
+
def test_atomic_write_doesnt_write_when_block_raises
File.atomic_write(file_name) do |file|
file.write("testing")
@@ -22,8 +21,47 @@ class AtomicWriteTest < Test::Unit::TestCase
rescue
assert !File.exist?(file_name)
end
-
- def file_name
- "atomic.file"
+
+ def test_atomic_write_preserves_file_permissions
+ contents = "Atomic Text"
+ File.open(file_name, "w", 0755) do |file|
+ file.write(contents)
+ assert File.exist?(file_name)
+ end
+ assert File.exist?(file_name)
+ assert_equal 0100755, file_mode
+ assert_equal contents, File.read(file_name)
+
+ File.atomic_write(file_name, Dir.pwd) do |file|
+ file.write(contents)
+ assert File.exist?(file_name)
+ end
+ assert File.exist?(file_name)
+ assert_equal 0100755, file_mode
+ assert_equal contents, File.read(file_name)
+ ensure
+ File.unlink(file_name) rescue nil
+ end
+
+ def test_atomic_write_preserves_default_file_permissions
+ contents = "Atomic Text"
+ File.atomic_write(file_name, Dir.pwd) do |file|
+ file.write(contents)
+ assert !File.exist?(file_name)
+ end
+ assert File.exist?(file_name)
+ assert_equal 0100666 ^ File.umask, file_mode
+ assert_equal contents, File.read(file_name)
+ ensure
+ File.unlink(file_name) rescue nil
end
+
+ private
+ def file_name
+ "atomic.file"
+ end
+
+ def file_mode
+ File.stat(file_name).mode
+ end
end
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index 69028a123f..fc8ed45358 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -245,6 +245,16 @@ class HashExtTest < Test::Unit::TestCase
assert(!indiff.keys.any? {|k| k.kind_of? String}, "A key was converted to a string!")
end
+ def test_deep_merge
+ hash_1 = { :a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } } }
+ hash_2 = { :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } }
+ expected = { :a => 1, :b => "b", :c => { :c1 => 2, :c2 => "c2", :c3 => { :d1 => "d1", :d2 => "d2" } } }
+ assert_equal expected, hash_1.deep_merge(hash_2)
+
+ hash_1.deep_merge!(hash_2)
+ assert_equal expected, hash_1
+ end
+
def test_reverse_merge
defaults = { :a => "x", :b => "y", :c => 10 }.freeze
options = { :a => 1, :b => 2 }
@@ -282,6 +292,27 @@ class HashExtTest < Test::Unit::TestCase
assert_equal expected, original
end
+ def test_slice_with_an_array_key
+ original = { :a => 'x', :b => 'y', :c => 10, [:a, :b] => "an array key" }
+ expected = { [:a, :b] => "an array key", :c => 10 }
+
+ # Should return a new hash with only the given keys when given an array key.
+ assert_equal expected, original.slice([:a, :b], :c)
+ assert_not_equal expected, original
+
+ # Should replace the hash with only the given keys when given an array key.
+ assert_equal expected, original.slice!([:a, :b], :c)
+ assert_equal expected, original
+ end
+
+ def test_slice_with_splatted_keys
+ original = { :a => 'x', :b => 'y', :c => 10, [:a, :b] => "an array key" }
+ expected = { :a => 'x', :b => "y" }
+
+ # Should grab each of the splatted keys.
+ assert_equal expected, original.slice(*[:a, :b])
+ end
+
def test_indifferent_slice
original = { :a => 'x', :b => 'y', :c => 10 }.with_indifferent_access
expected = { :a => 'x', :b => 'y' }.with_indifferent_access
diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object_and_class_ext_test.rb
index 16f4ab888e..b0a746fdc7 100644
--- a/activesupport/test/core_ext/object_and_class_ext_test.rb
+++ b/activesupport/test/core_ext/object_and_class_ext_test.rb
@@ -173,6 +173,14 @@ class ObjectTests < Test::Unit::TestCase
assert duck.acts_like?(:time)
assert !duck.acts_like?(:date)
end
+
+ def test_metaclass
+ string = "Hello"
+ string.metaclass.instance_eval do
+ define_method(:foo) { "bar" }
+ end
+ assert_equal "bar", string.foo
+ end
end
class ObjectInstanceVariableTest < Test::Unit::TestCase
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index 19a30f1730..c9f959ef32 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -32,6 +32,10 @@ class StringInflectionsTest < Test::Unit::TestCase
end
end
+ def test_camelize_lower
+ assert_equal('capital', 'Capital'.camelize(:lower))
+ end
+
def test_underscore
CamelToUnderscore.each do |camel, underscore|
assert_equal(underscore, camel.underscore)
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index 17a0968c0e..8740497b3d 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -528,8 +528,13 @@ class TimeExtCalculationsTest < Test::Unit::TestCase
assert_equal Time.time_with_datetime_fallback(:utc, 2005), Time.utc(2005)
assert_equal Time.time_with_datetime_fallback(:utc, 2039), DateTime.civil(2039, 1, 1, 0, 0, 0, 0, 0)
assert_equal Time.time_with_datetime_fallback(:utc, 2005, 2, 21, 17, 44, 30, 1), Time.utc(2005, 2, 21, 17, 44, 30, 1) #with usec
- assert_equal Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1), DateTime.civil(2039, 2, 21, 17, 44, 30, 0, 0)
- assert_equal ::Date::ITALY, Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1).start # use Ruby's default start value
+ # This won't overflow on 64bit linux
+ expected_to_overflow = Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1)
+ unless expected_to_overflow.is_a?(Time)
+ assert_equal Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1),
+ DateTime.civil(2039, 2, 21, 17, 44, 30, 0, 0)
+ assert_equal ::Date::ITALY, Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1).start # use Ruby's default start value
+ end
end
def test_utc_time
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index 038547a862..39c9c74c94 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -762,4 +762,16 @@ class DependenciesTest < Test::Unit::TestCase
ensure
ActiveSupport::Dependencies.load_once_paths = []
end
+
+ def test_hook_called_multiple_times
+ assert_nothing_raised { ActiveSupport::Dependencies.hook! }
+ end
+
+ def test_unhook
+ ActiveSupport::Dependencies.unhook!
+ assert !Module.new.respond_to?(:const_missing_without_dependencies)
+ assert !Module.new.respond_to?(:load_without_new_constant_marking)
+ ensure
+ ActiveSupport::Dependencies.hook!
+ end
end
diff --git a/activesupport/test/i18n_test.rb b/activesupport/test/i18n_test.rb
new file mode 100644
index 0000000000..4b17e3c523
--- /dev/null
+++ b/activesupport/test/i18n_test.rb
@@ -0,0 +1,75 @@
+require 'abstract_unit'
+
+class I18nTest < Test::Unit::TestCase
+ def setup
+ @date = Date.parse("2008-7-2")
+ @time = Time.utc(2008, 7, 2, 16, 47, 1)
+ end
+
+ uses_mocha 'I18nTimeZoneTest' do
+ def test_time_zone_localization_with_default_format
+ Time.zone.stubs(:now).returns Time.local(2000)
+ assert_equal Time.zone.now.strftime("%a, %d %b %Y %H:%M:%S %z"), I18n.localize(Time.zone.now)
+ end
+ end
+
+ def test_date_localization_should_use_default_format
+ assert_equal @date.strftime("%Y-%m-%d"), I18n.localize(@date)
+ end
+
+ def test_date_localization_with_default_format
+ assert_equal @date.strftime("%Y-%m-%d"), I18n.localize(@date, :format => :default)
+ end
+
+ def test_date_localization_with_short_format
+ assert_equal @date.strftime("%b %d"), I18n.localize(@date, :format => :short)
+ end
+
+ def test_date_localization_with_long_format
+ assert_equal @date.strftime("%B %d, %Y"), I18n.localize(@date, :format => :long)
+ end
+
+ def test_time_localization_should_use_default_format
+ assert_equal @time.strftime("%a, %d %b %Y %H:%M:%S %z"), I18n.localize(@time)
+ end
+
+ def test_time_localization_with_default_format
+ assert_equal @time.strftime("%a, %d %b %Y %H:%M:%S %z"), I18n.localize(@time, :format => :default)
+ end
+
+ def test_time_localization_with_short_format
+ assert_equal @time.strftime("%d %b %H:%M"), I18n.localize(@time, :format => :short)
+ end
+
+ def test_time_localization_with_long_format
+ assert_equal @time.strftime("%B %d, %Y %H:%M"), I18n.localize(@time, :format => :long)
+ end
+
+ def test_day_names
+ assert_equal Date::DAYNAMES, I18n.translate(:'date.day_names')
+ end
+
+ def test_abbr_day_names
+ assert_equal Date::ABBR_DAYNAMES, I18n.translate(:'date.abbr_day_names')
+ end
+
+ def test_month_names
+ assert_equal Date::MONTHNAMES, I18n.translate(:'date.month_names')
+ end
+
+ def test_abbr_month_names
+ assert_equal Date::ABBR_MONTHNAMES, I18n.translate(:'date.abbr_month_names')
+ end
+
+ def test_date_order
+ assert_equal [:year, :month, :day], I18n.translate(:'date.order')
+ end
+
+ def test_time_am
+ assert_equal 'am', I18n.translate(:'time.am')
+ end
+
+ def test_time_pm
+ assert_equal 'pm', I18n.translate(:'time.pm')
+ end
+end
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index 6c0c14e866..8eebe1be25 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -34,6 +34,13 @@ class InflectorTest < Test::Unit::TestCase
end
end
+ def test_overwrite_previous_inflectors
+ assert_equal("series", ActiveSupport::Inflector.singularize("series"))
+ ActiveSupport::Inflector.inflections.singular "series", "serie"
+ assert_equal("serie", ActiveSupport::Inflector.singularize("series"))
+ ActiveSupport::Inflector.inflections.uncountable "series" # Return to normal
+ end
+
MixtureToTitleCase.each do |before, titleized|
define_method "test_titleize_#{before}" do
assert_equal(titleized, ActiveSupport::Inflector.titleize(before))
@@ -46,6 +53,10 @@ class InflectorTest < Test::Unit::TestCase
end
end
+ def test_camelize_with_lower_downcases_the_first_letter
+ assert_equal('capital', ActiveSupport::Inflector.camelize('Capital', false))
+ end
+
def test_underscore
CamelToUnderscore.each do |camel, underscore|
assert_equal(underscore, ActiveSupport::Inflector.underscore(camel))
diff --git a/activesupport/test/memoizable_test.rb b/activesupport/test/memoizable_test.rb
index fc24a2942d..135d56f14a 100644
--- a/activesupport/test/memoizable_test.rb
+++ b/activesupport/test/memoizable_test.rb
@@ -3,47 +3,211 @@ require 'abstract_unit'
uses_mocha 'Memoizable' do
class MemoizableTest < Test::Unit::TestCase
class Person
- include ActiveSupport::Memoizable
+ extend ActiveSupport::Memoizable
+
+ attr_reader :name_calls, :age_calls
+ def initialize
+ @name_calls = 0
+ @age_calls = 0
+ end
def name
- fetch_name_from_floppy
+ @name_calls += 1
+ "Josh"
+ end
+
+ def name?
+ true
end
- memoize :name
+ memoize :name?
+
+ def update(name)
+ "Joshua"
+ end
+ memoize :update
def age
+ @age_calls += 1
nil
end
- memoize :age
- private
- def fetch_name_from_floppy
- "Josh"
+ memoize :name, :age
+ end
+
+ class Company
+ attr_reader :name_calls
+ def initialize
+ @name_calls = 0
+ end
+
+ def name
+ @name_calls += 1
+ "37signals"
+ end
+ end
+
+ module Rates
+ extend ActiveSupport::Memoizable
+
+ attr_reader :sales_tax_calls
+ def sales_tax(price)
+ @sales_tax_calls ||= 0
+ @sales_tax_calls += 1
+ price * 0.1025
+ end
+ memoize :sales_tax
+ end
+
+ class Calculator
+ extend ActiveSupport::Memoizable
+ include Rates
+
+ attr_reader :fib_calls
+ def initialize
+ @fib_calls = 0
+ end
+
+ def fib(n)
+ @fib_calls += 1
+
+ if n == 0 || n == 1
+ n
+ else
+ fib(n - 1) + fib(n - 2)
end
+ end
+ memoize :fib
+
+ def counter
+ @count ||= 0
+ @count += 1
+ end
+ memoize :counter
+ end
+
+ def setup
+ @person = Person.new
+ @calculator = Calculator.new
end
def test_memoization
- person = Person.new
- assert_equal "Josh", person.name
+ assert_equal "Josh", @person.name
+ assert_equal 1, @person.name_calls
- person.expects(:fetch_name_from_floppy).never
- 2.times { assert_equal "Josh", person.name }
+ 3.times { assert_equal "Josh", @person.name }
+ assert_equal 1, @person.name_calls
end
- def test_memoized_methods_are_frozen
- person = Person.new
- person.freeze
- assert_equal "Josh", person.name
- assert_equal true, person.name.frozen?
+ def test_memoization_with_punctuation
+ assert_equal true, @person.name?
end
- def test_memoization_frozen_with_nil_value
- person = Person.new
- person.freeze
- assert_equal nil, person.age
+ def test_memoization_with_nil_value
+ assert_equal nil, @person.age
+ assert_equal 1, @person.age_calls
+
+ 3.times { assert_equal nil, @person.age }
+ assert_equal 1, @person.age_calls
+ end
+
+ def test_memorized_results_are_immutable
+ assert_equal "Josh", @person.name
+ assert_raise(ActiveSupport::FrozenObjectError) { @person.name.gsub!("Josh", "Gosh") }
+ end
+
+ def test_reloadable
+ counter = @calculator.counter
+ assert_equal 1, @calculator.counter
+ assert_equal 2, @calculator.counter(:reload)
+ assert_equal 2, @calculator.counter
+ assert_equal 3, @calculator.counter(true)
+ assert_equal 3, @calculator.counter
+ end
+
+ def test_unmemoize_all
+ assert_equal 1, @calculator.counter
+
+ assert @calculator.instance_variable_get(:@_memoized_counter).any?
+ @calculator.unmemoize_all
+ assert @calculator.instance_variable_get(:@_memoized_counter).empty?
+
+ assert_equal 2, @calculator.counter
+ end
+
+ def test_memoize_all
+ @calculator.memoize_all
+ assert @calculator.instance_variable_defined?(:@_memoized_counter)
+ end
+
+ def test_memoization_cache_is_different_for_each_instance
+ assert_equal 1, @calculator.counter
+ assert_equal 2, @calculator.counter(:reload)
+ assert_equal 1, Calculator.new.counter
+ end
+
+ def test_memoized_is_not_affected_by_freeze
+ @person.freeze
+ assert_equal "Josh", @person.name
+ assert_equal "Joshua", @person.update("Joshua")
+ end
+
+ def test_memoization_with_args
+ assert_equal 55, @calculator.fib(10)
+ assert_equal 11, @calculator.fib_calls
+ end
+
+ def test_reloadable_with_args
+ assert_equal 55, @calculator.fib(10)
+ assert_equal 11, @calculator.fib_calls
+ assert_equal 55, @calculator.fib(10, :reload)
+ assert_equal 12, @calculator.fib_calls
+ assert_equal 55, @calculator.fib(10, true)
+ assert_equal 13, @calculator.fib_calls
+ end
+
+ def test_object_memoization
+ [Company.new, Company.new, Company.new].each do |company|
+ company.extend ActiveSupport::Memoizable
+ company.memoize :name
+
+ assert_equal "37signals", company.name
+ assert_equal 1, company.name_calls
+ assert_equal "37signals", company.name
+ assert_equal 1, company.name_calls
+ end
+ end
+
+ def test_memoized_module_methods
+ assert_equal 1.025, @calculator.sales_tax(10)
+ assert_equal 1, @calculator.sales_tax_calls
+ assert_equal 1.025, @calculator.sales_tax(10)
+ assert_equal 1, @calculator.sales_tax_calls
+ assert_equal 2.5625, @calculator.sales_tax(25)
+ assert_equal 2, @calculator.sales_tax_calls
+ end
+
+ def test_object_memoized_module_methods
+ company = Company.new
+ company.extend(Rates)
+
+ assert_equal 1.025, company.sales_tax(10)
+ assert_equal 1, company.sales_tax_calls
+ assert_equal 1.025, company.sales_tax(10)
+ assert_equal 1, company.sales_tax_calls
+ assert_equal 2.5625, company.sales_tax(25)
+ assert_equal 2, company.sales_tax_calls
end
def test_double_memoization
assert_raise(RuntimeError) { Person.memoize :name }
+ person = Person.new
+ person.extend ActiveSupport::Memoizable
+ assert_raise(RuntimeError) { person.memoize :name }
+
+ company = Company.new
+ company.extend ActiveSupport::Memoizable
+ company.memoize :name
+ assert_raise(RuntimeError) { company.memoize :name }
end
end
end
diff --git a/activesupport/test/option_merger_test.rb b/activesupport/test/option_merger_test.rb
index 509c6d3bad..0d72314880 100644
--- a/activesupport/test/option_merger_test.rb
+++ b/activesupport/test/option_merger_test.rb
@@ -38,6 +38,33 @@ class OptionMergerTest < Test::Unit::TestCase
end
end
+ def test_nested_method_with_options_containing_hashes_merge
+ with_options :conditions => { :method => :get } do |outer|
+ outer.with_options :conditions => { :domain => "www" } do |inner|
+ expected = { :conditions => { :method => :get, :domain => "www" } }
+ assert_equal expected, inner.method_with_options
+ end
+ end
+ end
+
+ def test_nested_method_with_options_containing_hashes_overwrite
+ with_options :conditions => { :method => :get, :domain => "www" } do |outer|
+ outer.with_options :conditions => { :method => :post } do |inner|
+ expected = { :conditions => { :method => :post, :domain => "www" } }
+ assert_equal expected, inner.method_with_options
+ end
+ end
+ end
+
+ def test_nested_method_with_options_containing_hashes_going_deep
+ with_options :html => { :class => "foo", :style => { :margin => 0, :display => "block" } } do |outer|
+ outer.with_options :html => { :title => "bar", :style => { :margin => "1em", :color => "#fff" } } do |inner|
+ expected = { :html => { :class => "foo", :title => "bar", :style => { :margin => "1em", :display => "block", :color => "#fff" } } }
+ assert_equal expected, inner.method_with_options
+ end
+ end
+ end
+
# Needed when counting objects with the ObjectSpace
def test_option_merger_class_method
assert_equal ActiveSupport::OptionMerger, ActiveSupport::OptionMerger.new('', '').class
diff --git a/ci/ci_build.rb b/ci/ci_build.rb
new file mode 100755
index 0000000000..7b9cdceb27
--- /dev/null
+++ b/ci/ci_build.rb
@@ -0,0 +1,116 @@
+#!/usr/bin/env ruby
+require 'fileutils'
+
+include FileUtils
+
+puts "[CruiseControl] Rails build"
+
+build_results = {}
+root_dir = File.expand_path(File.dirname(__FILE__) + "/..")
+
+# Requires gem home and path to be writeable and/or overridden to be ~/.gem,
+# Will enable when RubyGems supports this properly (in a coming release)
+# build_results[:geminstaller] = system 'geminstaller --exceptions'
+
+# for now, use the no-passwd sudoers approach (documented in ci_setup_notes.txt)
+# A security hole, but there is nothing valuable on rails CI box anyway.
+build_results[:geminstaller] = system "sudo geminstaller --config=#{root_dir}/ci/geminstaller.yml --exceptions"
+
+cd "#{root_dir}/activesupport" do
+ puts
+ puts "[CruiseControl] Building ActiveSupport"
+ puts
+ build_results[:activesupport] = system 'rake'
+end
+
+cd "#{root_dir}/activerecord" do
+ puts
+ puts "[CruiseControl] Building ActiveRecord with MySQL"
+ puts
+ build_results[:activerecord_mysql] = system 'rake test_mysql'
+end
+
+cd "#{root_dir}/activerecord" do
+ puts
+ puts "[CruiseControl] Building ActiveRecord with PostgreSQL"
+ puts
+ build_results[:activerecord_postgresql8] = system 'rake test_postgresql'
+end
+
+# Sqlite2 is disabled until tests are fixed
+# cd "#{root_dir}/activerecord" do
+# puts
+# puts "[CruiseControl] Building ActiveRecord with SQLite 2"
+# puts
+# build_results[:activerecord_sqlite] = system 'rake test_sqlite'
+# end
+
+cd "#{root_dir}/activerecord" do
+ puts
+ puts "[CruiseControl] Building ActiveRecord with SQLite 3"
+ puts
+ build_results[:activerecord_sqlite3] = system 'rake test_sqlite3'
+end
+
+cd "#{root_dir}/activemodel" do
+ puts
+ puts "[CruiseControl] Building ActiveModel"
+ puts
+ build_results[:activemodel] = system 'rake'
+end
+
+cd "#{root_dir}/activeresource" do
+ puts
+ puts "[CruiseControl] Building ActiveResource"
+ puts
+ build_results[:activeresource] = system 'rake'
+end
+
+cd "#{root_dir}/actionpack" do
+ puts
+ puts "[CruiseControl] Building ActionPack"
+ puts
+ build_results[:actionpack] = system 'rake'
+end
+
+cd "#{root_dir}/actionmailer" do
+ puts
+ puts "[CruiseControl] Building ActionMailer"
+ puts
+ build_results[:actionmailer] = system 'rake'
+end
+
+cd "#{root_dir}/railties" do
+ puts
+ puts "[CruiseControl] Building RailTies"
+ puts
+ build_results[:railties] = system 'rake'
+end
+
+
+puts
+puts "[CruiseControl] Build environment:"
+puts "[CruiseControl] #{`cat /etc/issue`}"
+puts "[CruiseControl] #{`uname -a`}"
+puts "[CruiseControl] #{`ruby -v`}"
+puts "[CruiseControl] #{`mysql --version`}"
+puts "[CruiseControl] #{`pg_config --version`}"
+puts "[CruiseControl] SQLite2: #{`sqlite -version`}"
+puts "[CruiseControl] SQLite3: #{`sqlite3 -version`}"
+`gem env`.each {|line| print "[CruiseControl] #{line}"}
+puts "[CruiseControl] Local gems:"
+`gem list`.each {|line| print "[CruiseControl] #{line}"}
+
+failures = build_results.select { |key, value| value == false }
+
+if failures.empty?
+ puts
+ puts "[CruiseControl] Rails build finished sucessfully"
+ exit(0)
+else
+ puts
+ puts "[CruiseControl] Rails build FAILED"
+ puts "[CruiseControl] Failed components: #{failures.map { |component| component.first }.join(', ')}"
+ exit(-1)
+end
+
diff --git a/ci/ci_setup_notes.txt b/ci/ci_setup_notes.txt
new file mode 100644
index 0000000000..86df33c443
--- /dev/null
+++ b/ci/ci_setup_notes.txt
@@ -0,0 +1,120 @@
+# Rails Continuous Integration Server Setup Notes
+# This procedure was used to set up http://ci.rubyonrails.org on Ubuntu 8.04
+# It can be used as a guideline for setting up your own CI server against your local rails branches
+
+* Set up ci user:
+# log in as root
+$ adduser ci
+enter user info and password
+$ visudo
+# give ci user same sudo rights as root
+
+* Disable root login:
+# log in as ci
+$ sudo vi /etc/shadow
+# overwrite and disable encrypted root password to disable root login:
+root:*:14001:0:99999:7:::
+
+* Change Hostname:
+$ sudo vi /etc/hostname
+change to 'ci'
+$ sudo vi /etc/hosts
+replace old hostname with 'ci'
+# reboot to use new hostname (and test reboot)
+$ sudo shutdown -r now
+
+* Update aptitude:
+$ sudo aptitude update
+
+* Use cinabox to perform rest of ruby/ccrb setup:
+* http://github.com/thewoolleyman/cinabox/tree/master/README.txt
+
+# This is not yet properly supported by RubyGems...
+# * Configure RubyGems to not require root access for gem installation
+# $ vi ~/.profile
+# # add this line at bottom:
+# PATH="$HOME/.gem/ruby/1.8/bin:$PATH"
+# $ sudo vi /etc/init.d/cruise
+# # edit the start_cruise line to source CRUISE_USER/.profile:
+# start_cruise "cd #{CRUISE_HOME} && source /home/#{CRUISE_USER}/.profile && ./cruise start -d"
+# $ vi ~/.gemrc
+# # add these lines:
+# ---
+# gemhome: /home/ci/.gem/ruby/1.8
+# gempath:
+# - /home/ci/.gem/ruby/1.8
+
+* If you did not configure no-root-gem installation via ~/.gemrc as shown above, then allow no-password sudo for gem installation:
+$ sudo visudo
+# add this line to bottom:
+ci ALL=NOPASSWD: /usr/local/bin/geminstaller, /usr/local/bin/ruby, /usr/local/bin/gem
+
+* Start ccrb via init script and check for default homepage at port 3333
+
+* Install/setup nginx:
+$ sudo aptitude install nginx
+$ sudo vi /etc/nginx/sites-available/default
+# comment two lines and add one to proxy to ccrb:
+# root /var/www/nginx-default;
+# index index.html index.htm;
+ proxy_pass http://127.0.0.1:3333;
+$ sudo /etc/init.d/nginx start
+
+* Add project to cruise (It will still fail until everything is set up):
+$ cd ~/ccrb
+$ ./cruise add rails -s git -r git://github.com/rails/rails.git # or the URI of your branch
+
+* Copy and configure cruise site config file:
+$ cp ~/.cruise/projects/rails/work/ci/site_config.rb ~/.cruise/site_config.rb
+# Edit ~/.cruise/site_config.rb as desired, for example:
+ActionMailer::Base.smtp_settings = {
+ :address => "localhost",
+ :domain => "ci.yourdomain.com",
+}
+Configuration.dashboard_refresh_interval = 60.seconds
+Configuration.dashboard_url = 'http://ci.yourdomain.com/'
+Configuration.serialize_builds = true
+Configuration.serialized_build_timeout = 1.hours
+BuildReaper.number_of_builds_to_keep = 100
+
+* Copy and configure cruise project config file
+$ cp ~/.cruise/projects/rails/work/ci/cruise_config.rb ~/.cruise/projects/rails
+$ vi ~/.cruise/projects/rails/cruise_config.rb:
+# Edit ~/.cruise/projects/rails/cruise_config.rb as desired, for example:
+Project.configure do |project|
+ project.build_command = 'ruby ci/ci_build.rb'
+ project.email_notifier.emails = ['recipient@yourdomain.com']
+ project.email_notifier.from = 'sender@yourdomain.com'
+end
+
+* Set up mysql
+$ sudo aptitude install mysql-server-5.0 libmysqlclient-dev
+# no password for mysql root user
+
+* setup sqlite
+$ sudo aptitude install sqlite sqlite3 libsqlite-dev libsqlite3-dev
+# Note: there's some installation bugs with sqlite3-ruby 1.2.2 gem file permissions:
+# http://www.icoretech.org/2008/07/06/no-such-file-to-load-sqlite3-database
+# cd /usr/local/lib/ruby/gems/1.8/gems/sqlite3-ruby-1.2.2 && sudo find . -perm 0662 -exec chmod 664 {} \;
+
+* setup postgres
+$ sudo aptitude install postgresql postgresql-server-dev-8.3
+$ sudo su - postgres -c 'createuser -s ci'
+
+* Install and run GemInstaller to get all dependency gems
+$ sudo gem install geminstaller
+$ cd ~/.cruise/projects/rails/work
+$ sudo geminstaller --config=ci/geminstaller.yml # turn up debugging with these options: --geminstaller-output=all --rubygems-output=all
+
+* Create ActiveRecord test databases for mysql
+$ mysql -uroot -e 'grant all on *.* to rails@localhost;'
+$ mysql -urails -e 'create database activerecord_unittest;'
+$ mysql -urails -e 'create database activerecord_unittest2;'
+
+* Create ActiveRecord test databases for postgres
+# cd to rails activerecord dir
+$ rake postgresql:build_databases
+
+* Reboot and make sure everything is working
+$ sudo shutdown -r now
+$ http://ci.yourdomain.com \ No newline at end of file
diff --git a/ci/cruise_config.rb b/ci/cruise_config.rb
new file mode 100644
index 0000000000..ec1c82a8bf
--- /dev/null
+++ b/ci/cruise_config.rb
@@ -0,0 +1,5 @@
+Project.configure do |project|
+ project.build_command = 'ruby ci/ci_build.rb'
+ project.email_notifier.emails = ['thewoolleyman@gmail.com','michael@koziarski.com']
+ project.email_notifier.from = 'thewoolleyman+railsci@gmail.com'
+end
diff --git a/ci/geminstaller.yml b/ci/geminstaller.yml
new file mode 100644
index 0000000000..3a1862c3c3
--- /dev/null
+++ b/ci/geminstaller.yml
@@ -0,0 +1,17 @@
+---
+gems:
+- name: geminstaller
+ version: >= 0.4.3
+- name: mocha
+ version: >= 0.9.0
+- name: mysql
+ #version: >= 2.7
+ version: = 2.7
+- name: postgres
+ version: >= 0.7.9.2008.01.28
+- name: rake
+ version: >= 0.8.1
+- name: sqlite-ruby
+ version: >= 2.2.3
+- name: sqlite3-ruby
+ version: >= 1.2.2
diff --git a/ci/site.css b/ci/site.css
new file mode 100644
index 0000000000..e771c5d1fd
--- /dev/null
+++ b/ci/site.css
@@ -0,0 +1,13 @@
+/* this is a copy of /home/ci/.cruise/site.css, please make any changes to it there */
+
+/* this is a copy of /home/ci/.cruise/site.css, please make any changes to it there */
+
+/* if you'd like to add custom styles to cruise, add them here */
+/* the following will make successful builds green */
+a.success, a.success:visited {
+ color: #0A0;
+}
+
+.build_success {
+ background-image: url(/images/green_gradient.png);
+}
diff --git a/ci/site_config.rb b/ci/site_config.rb
new file mode 100644
index 0000000000..47b7e92120
--- /dev/null
+++ b/ci/site_config.rb
@@ -0,0 +1,72 @@
+# site_config.rb contains examples of various configuration options for the local installation
+# of CruiseControl.rb.
+
+# YOU MUST RESTART YOUR CRUISE CONTROL SERVER FOR ANY CHANGES MADE HERE TO TAKE EFFECT!!!
+
+# EMAIL NOTIFICATION
+# ------------------
+
+# CruiseControl.rb can notify you about build status via email. It uses ActionMailer component of Ruby on Rails
+# framework. Obviously, ActionMailer needs to know how to send out email messages.
+# If you have an SMTP server on your network, and it needs no authentication, write this in your site_config.rb:
+#
+ActionMailer::Base.smtp_settings = {
+ :address => "localhost",
+ :domain => "ci.rubyonrails.org",
+}
+#
+# If you have no SMTP server at hand, you can configure email notification to use GMail SMTP server, as follows
+# (of course, you'll need to create a GMail account):
+#
+# ActionMailer::Base.smtp_settings = {
+# :address => "smtp.gmail.com",
+# :port => 587,
+# :domain => "yourdomain.com",
+# :authentication => :plain,
+# :user_name => "yourgmailaccount",
+# :password => "yourgmailpassword"
+# }
+#
+# The same approach works for other SMTP servers thet require authentication. Note that GMail's SMTP server runs on a
+# non-standard port 587 (standard port for SMTP is 25).
+#
+# For further details about configuration of outgoing email, see Ruby On Rails documentation for ActionMailer::Base.
+
+# Other site-wide options are available through Configuration class:
+
+# Change how often CC.rb pings Subversion for new requests. Default is 10.seconds, which should be OK for a local
+# SVN repository, but probably isn't very polite for a public repository, such as RubyForge. This can also be set for
+# each project individually, through project.scheduler.polling_interval option:
+# Configuration.default_polling_interval = 1.minute
+
+# How often the dashboard page refreshes itself. If you have more than 10-20 dashboards open,
+# it is advisable to set it to something higher than the default 5 seconds:
+Configuration.dashboard_refresh_interval = 60.seconds
+
+# Site-wide setting for the email "from" field. This can also be set on per-project basis,
+# through project.email.notifier.from attribute
+Configuration.email_from = 'thewoolleyman+railsci@gmail.com'
+
+# Root URL of the dashboard application. Setting this attribute allows various notifiers to include a link to the
+# build page in the notification message.
+Configuration.dashboard_url = 'http://ci.rubyonrails.org/'
+
+# If you don't want to allow triggering builds through dashboard Build Now button. Useful when you host CC.rb as a
+# public web site (such as http://cruisecontrolrb.thoughtworks.com/projects - try clicking on Build Now button there
+# and see what happens):
+# Configuration.disable_build_now = true
+
+# If you want to only allow one project to build at a time, uncomment this line
+# by default, cruise allows multiple projects to build at a time
+Configuration.serialize_builds = true
+
+# Amount of time a project will wait to build before failing when build serialization is on
+Configuration.serialized_build_timeout = 3.hours
+
+# To delete build when there are more than a certain number present, uncomment this line - it will make the dashboard
+# perform better
+BuildReaper.number_of_builds_to_keep = 100
+
+# any files that you'd like to override in cruise, keep in ~/.cruise, and copy over when this file is loaded like this
+site_css = CRUISE_DATA_ROOT + "/site.css"
+FileUtils.cp site_css, RAILS_ROOT + "/public/stylesheets/site.css" if File.exists? site_css
diff --git a/railties/CHANGELOG b/railties/CHANGELOG
index b5c5aba460..3a276d5aad 100644
--- a/railties/CHANGELOG
+++ b/railties/CHANGELOG
@@ -1,5 +1,13 @@
*Edge*
+* Added config.threadsafe! to toggle allow concurrency settings and disable the dependency loader [Josh Peek]
+
+* Turn cache_classes on by default [Josh Peek]
+
+* Added configurable eager load paths. Defaults to app/models, app/controllers, and app/helpers [Josh Peek]
+
+* Introduce simple internationalization support. [Ruby i18n team]
+
* Make script/plugin install <plugin> -r <revision> option work with git based plugins. #257. [Tim Pope Jakub Kuźma]. Example:
script/plugin install git://github.com/mislav/will_paginate.git -r agnostic # Installs 'agnostic' branch
diff --git a/railties/Rakefile b/railties/Rakefile
index f64b60b0dd..174c85b59a 100644
--- a/railties/Rakefile
+++ b/railties/Rakefile
@@ -272,6 +272,21 @@ 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"
+ guides_html_files << output
+ file output => Dir["doc/guides/#{guide_name}/*.txt"] do
+ sh "mizuho", input, "--template", "manualsonrails", "--multi-page",
+ "--icons-dir", "../icons"
+ end
+end
+
+desc "Generate HTML output for the guides"
+task :generate_guides => guides_html_files
+
# Generate GEM ----------------------------------------------------------------------------
task :copy_gem_environment do
diff --git a/railties/bin/about b/railties/bin/about
index ed8deb0dfc..ed8deb0dfc 100644..100755
--- a/railties/bin/about
+++ b/railties/bin/about
diff --git a/railties/bin/console b/railties/bin/console
index 498077ab33..498077ab33 100644..100755
--- a/railties/bin/console
+++ b/railties/bin/console
diff --git a/railties/bin/destroy b/railties/bin/destroy
index a4df765a39..a4df765a39 100644..100755
--- a/railties/bin/destroy
+++ b/railties/bin/destroy
diff --git a/railties/bin/generate b/railties/bin/generate
index 173a9f147d..173a9f147d 100644..100755
--- a/railties/bin/generate
+++ b/railties/bin/generate
diff --git a/railties/bin/performance/benchmarker b/railties/bin/performance/benchmarker
index c842d35d33..c842d35d33 100644..100755
--- a/railties/bin/performance/benchmarker
+++ b/railties/bin/performance/benchmarker
diff --git a/railties/bin/performance/profiler b/railties/bin/performance/profiler
index d855ac8b13..d855ac8b13 100644..100755
--- a/railties/bin/performance/profiler
+++ b/railties/bin/performance/profiler
diff --git a/railties/bin/performance/request b/railties/bin/performance/request
index ae3f38c74b..ae3f38c74b 100644..100755
--- a/railties/bin/performance/request
+++ b/railties/bin/performance/request
diff --git a/railties/bin/plugin b/railties/bin/plugin
index 87cd2070fe..87cd2070fe 100644..100755
--- a/railties/bin/plugin
+++ b/railties/bin/plugin
diff --git a/railties/bin/process/inspector b/railties/bin/process/inspector
index bf25ad86d1..bf25ad86d1 100644..100755
--- a/railties/bin/process/inspector
+++ b/railties/bin/process/inspector
diff --git a/railties/bin/process/reaper b/railties/bin/process/reaper
index c77f04535f..c77f04535f 100644..100755
--- a/railties/bin/process/reaper
+++ b/railties/bin/process/reaper
diff --git a/railties/bin/process/spawner b/railties/bin/process/spawner
index 7118f3983c..7118f3983c 100644..100755
--- a/railties/bin/process/spawner
+++ b/railties/bin/process/spawner
diff --git a/railties/bin/runner b/railties/bin/runner
index a4a7cb25ba..a4a7cb25ba 100644..100755
--- a/railties/bin/runner
+++ b/railties/bin/runner
diff --git a/railties/bin/server b/railties/bin/server
index 3c67f39b69..3c67f39b69 100644..100755
--- a/railties/bin/server
+++ b/railties/bin/server
diff --git a/railties/config.ru b/railties/config.ru
new file mode 100644
index 0000000000..43492a2dcc
--- /dev/null
+++ b/railties/config.ru
@@ -0,0 +1,17 @@
+# Rackup Configuration
+#
+# Start Rails mongrel server with rackup
+# $ rackup -p 3000 config.ru
+#
+# Start server with webrick (or any compatible Rack server) instead
+# $ rackup -p 3000 -s webrick config.ru
+
+# Require your environment file to bootstrap Rails
+require File.dirname(__FILE__) + '/config/environment'
+
+# Static server middleware
+# You can remove this extra check if you use an asset server
+use Rails::Rack::Static
+
+# Dispatch the request
+run ActionController::Dispatcher.new
diff --git a/railties/configs/apache.conf b/railties/configs/apache.conf
index d9d211c058..d9d211c058 100755..100644
--- a/railties/configs/apache.conf
+++ b/railties/configs/apache.conf
diff --git a/railties/dispatches/dispatch.fcgi b/railties/dispatches/dispatch.fcgi
index 65188f380b..664dbbbee8 100755
--- a/railties/dispatches/dispatch.fcgi
+++ b/railties/dispatches/dispatch.fcgi
@@ -1,4 +1,4 @@
-#!/usr/local/bin/ruby
+#!/usr/bin/env ruby
#
# You may specify the path to the FastCGI crash log (a log of unhandled
# exceptions which forced the FastCGI instance to exit, great for debugging)
diff --git a/railties/dispatches/dispatch.rb b/railties/dispatches/dispatch.rb
index 9b5ae760ff..32fa3b2665 100755
--- a/railties/dispatches/dispatch.rb
+++ b/railties/dispatches/dispatch.rb
@@ -1,4 +1,4 @@
-#!/usr/local/bin/ruby
+#!/usr/bin/env ruby
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
@@ -7,4 +7,4 @@ require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_
require "dispatcher"
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
-Dispatcher.dispatch \ No newline at end of file
+Dispatcher.dispatch
diff --git a/railties/dispatches/gateway.cgi b/railties/dispatches/gateway.cgi
index fb1fa22f04..0305b7f810 100644..100755
--- a/railties/dispatches/gateway.cgi
+++ b/railties/dispatches/gateway.cgi
@@ -1,4 +1,4 @@
-#!/usr/local/bin/ruby
+#!/usr/bin/env ruby
require 'drb'
@@ -94,4 +94,4 @@ end
$stdout.write output
$stdout.flush
-$stdout.close \ No newline at end of file
+$stdout.close
diff --git a/railties/doc/guides/actionview/helpers.markdown b/railties/doc/guides/actionview/helpers.markdown
new file mode 100644
index 0000000000..c702e83ff9
--- /dev/null
+++ b/railties/doc/guides/actionview/helpers.markdown
@@ -0,0 +1,91 @@
+Helpers
+====================
+
+Helper Basics
+------------------------
+
+Helpers allow you to encapsulate rendering tasks as reusable functions. Helpers are modules, not classes, so their methods execute in the context in which they are called. They get included in a controller (typically the ApplicationController) using the helper function, like so
+
+ Class ApplicationController < ActionController::Base
+ …
+ helper :menu
+
+ def …
+ end
+ end
+
+In this way, methods in the menu helper are made available to any view or partial in your application. These methods can accept parameters, for example controller instance variables (eg; records or record collections gathered by you current controller), items from the view or partial’s locals[] hash or items from the params[] hash. You may wish to pass your controller instance variables and items from the params[] hash to the locals hash before rendering (See the section on partials). Helper methods can also accept an executable block of code.
+
+It is important to remember, though, that helpers are for rendering, and that they become available once a controller method has returned, while Rails is engaged in rendering the contents generated by a controller method. This means that helper methods are not available from within the methods of your controllers.
+
+Helpers can accomplish a variety of tasks, from formatting a complex tag for embedding content for a browser plugin (eg; Flash), to assembling a menu of options appropriate for the current context of your application, to generating sections of forms that get assembled on-the-fly.
+
+Helpers are organized around rendering tasks, so it is not necessary (nor necessarily desirable) to organize them around your application’s controllers or models. In fact, one of the benefits of helpers is that they are not connected via a rendering pipeline to specific controllers, like views and partials are. They can and should handle more generalized tasks.
+
+Here is a very simple, pseudo-example:
+
+ module MenuHelper
+ def menu(records, menu_options={})
+ item_options = menu_options.merge({<some stuff>})
+ items = records.collect |record| do
+ menu_item(record, options)
+ end
+ content_tag(“ul”, items, options)
+ end
+
+ def menu_item(record, item_options={}))
+ action = item_options[:action]
+ action ||= “show”
+ content_tag(“li”, link_to(record.title, :action => action, item_options)
+ end
+ end
+
+
+This helper will require that records passed into it have certain fields (notably :title). The helper could be written to use this as a default, allowing the field to be overwritten by an element of item_options.
+
+Look at the Rails API for examples of helpers included in Rails, eg; [`ActionView::Helpers::ActiveRecordHelper`](http://api.rubyonrails.org/classes/ActionView/Helpers/ActiveRecordHelper.html).
+
+Passing Blocks to Helper Methods
+------------------------
+
+We mentioned before that blocks can be passed to helper methods. This allows for an interesting wrinkle: a block passed to a helper method can cause it to render a partial, which can then be wrapped by the helper method’s output. This can make your helper method much more reusable. It doesn’t need to know anything about the internals about what it is rendering, it just contextualizes it for the page. You can also use the helper to modify the locals hash for the partial, based on some configuration information unique to the current controller. You could implement a flexible themes system in this way.
+
+
+Partials vs. Helpers?
+------------------------
+
+In general, the choice between using a partial vs. using a helper depends on the amount of flexibility you need. If the task is more about reacting to conditions than performing actual rendering, you may likely want a helper method. If you want to be able to call it from a variety of views, again, you may want to use a helper method. You can expect to extract helper methods out of code in views and partials during refactoring.
+
+
+Tutorial -- Calling a Helper [UNFINISHED]
+------------------------
+
+1. Create a Rails application using `rails helper_test`
+Notice the code:
+
+ class ApplicationController < ActionController::Base
+ helper :all # include all helpers, all the time
+For this tutorial, we'll keep this code, but you will likely want to exert more control over loading your helpers.
+
+2. Configure a database of your choice for the app.
+
+3. Inside of the `/app/helpers/` directory, create a new file called, `menu_helper.rb`. Write this in the file:
+
+ module MenuHelpers
+ def menu(records, item_proc=nil)
+ items = records.collect{ |record|
+ menu_item(record, item_proc)
+ }
+ content_tag("ul", items)
+ end
+
+ def menu_item(record, item_proc=nil)
+ item_url = item_proc.call(record)
+ item_url ||= { :action => :show }
+ content_tag("li", link_to(record.name, item_url))
+ end
+ end
+
+4. Create a scaffold for some object in your app, using `./script/generate scaffold widgets`.
+5. Create a database table for your widgets, with at least the fields `name` and `id`. Create a few widgets.
+6. Call the menu command twice from `index.html.erb`, once using the default action, and once supplying a Proc to generate urls. \ No newline at end of file
diff --git a/railties/doc/guides/actionview/partials.markdown b/railties/doc/guides/actionview/partials.markdown
new file mode 100644
index 0000000000..2988b933bc
--- /dev/null
+++ b/railties/doc/guides/actionview/partials.markdown
@@ -0,0 +1,90 @@
+A Guide to Using Partials
+===============================
+
+This guide elaborates on the use and function of partials in Ruby on Rails. As your Rails application grows, your view templates can start to contain a lot of duplicate view code. To manage and reduce this complexity, you can by abstract view template code into partials. Partials are reusable snippets of eRB template code stored in separate files with an underscore ('_') prefix.
+
+Partials can be located anywhere in the `app/views` directory. File extensions for partials work just like other template files, they bear an extension that denotes what kind of code they generate. For example, `_animal.html.erb` and `_animal.xml.erb` are valid filenames for partials.
+
+Partials can be inserted in eRB template code by calling the `render` method with the `:partial` option. For example:
+
+ <%= render :partial => 'foo' %>
+
+This inserts the result of evaluating the template `_foo.html.erb` into the parent template file at this location. Note that `render` assumes that the partial will be in the same directory as the calling parent template and have the same file extension. Partials can be located anywhere within the `app/views` directory. To use a partial located in a different directory then it the parent, add a '/' before it:
+
+ <%= render :partial => '/common/foo' %>
+
+Loads the partial file from the `app/views/common/_foo.html.erb` directory.
+
+Abstracting views into partials can be approached in a number of different ways, depending on the situation. Sometimes, the code that you are abstracting is a specialized view of an object or a collection of objects. Other times, you can look at partials as a reusable subroutine. We'll explore each of these approaches and when to use them as well as the syntax for calling them.
+
+Partials as a View Subroutine
+-----------------------------
+
+Using the `:locals` option, you can pass a hash of values which will be treated as local variables within the partial template.
+
+ <%= render :partial => "person", :locals => { :name => "david" } %>
+
+The variable `name` contains the value `"david"` within the `_person.html.erb` template. Passing variables in this way allows you to create modular, reusable template files. Note that if you want to make local variables that are optional in some use cases, you will have to set them to a sentinel value such as `nil` when they have not been passed. So, in the example above, if the `name` variable is optional in some use cases, you must set:
+
+ <% name ||= nil -%>
+
+So that you can later check:
+
+ <% if name -%>
+ <p>Hello, <%= name %>!</p>
+ <% end -%>
+
+Otherwise, the if statement will throw an error at runtime.
+
+Another thing to be aware of is that instance variables that are visible to the parent view template are visible to the partial. So you might be tempted to do this:
+
+ <%= render :partial => "person" %>
+
+And then within the partial:
+
+ <% if @name -%>
+ <p>Hello, <%= @name %>!</p>
+ <% end -%>
+
+The potential snag here is that if multiple templates start to rely on this partial, you will need to maintain an instance variable with the same name across all of these templates and controllers. This approach can quickly become brittle if overused.
+
+Partials as a View of an Object
+--------------------------------
+
+Another way to look at partials is to view them as mini-views of a particular object or instance variable. Use the `:object` option to pass an object assigns that object to an instance variable named after the partial itself. For example:
+
+ # Renders the partial, making @new_person available through
+ # the local variable 'person'
+ render :partial => "person", :object => @new_person
+
+If the instance variable `name` in the parent template matches the name of the partial, you can use a shortcut:
+
+ render :partial => "person"
+
+Now the value that was in `@person` in the parent template is accessible as `person` in the partial.
+
+Partials as a View of a Collection
+-----------------------------------
+
+Often it is the case that you need to display not just a single object, but a collection of objects. Rather than having to constantly nest the same partial within the same iterator code, Rails provides a syntactical shortcut using the `:collection` option:
+
+ # Renders a collection of the same partial by making each element
+ # of @winners available through the local variable "person" as it
+ # builds the complete response.
+ render :partial => "person", :collection => @winners
+
+This calls the `_person.html.erb` partial for each person in the `@winners` collection. This also creates a local variable within the partial called `partial_counter` which contains the index of the current value. So for example:
+
+ <%= partial_counter %>) <%= person -%>
+
+Where `@winners` contains three people, produces the following output:
+
+ 1) Bill
+ 2) Jeff
+ 3) Nick
+
+One last detail, you can place an arbitrary snippet of code in between the objects using the `:spacer_template` option.
+
+ # Renders the same collection of partials, but also renders the
+ # person_divider partial between each person partial.
+ render :partial => "person", :collection => @winners, :spacer_template => "person_divider"
diff --git a/railties/doc/guides/activerecord/basics.markdown b/railties/doc/guides/activerecord/basics.markdown
new file mode 100644
index 0000000000..0d030fabf9
--- /dev/null
+++ b/railties/doc/guides/activerecord/basics.markdown
@@ -0,0 +1,56 @@
+Active Record Basics
+====================
+
+
+
+The ActiveRecord Pattern
+------------------------
+
+Active Record (the library) conforms to the active record design pattern. The active record pattern is a design pattern often found in applications that use relational database. The name comes from by Martin Fowler's book *Patterns of Enterprise Application Architecture*, in which he describes an active record object as:
+
+> An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.
+
+So, an object that follows the active record pattern encapsulates both data and behavior; in other words, they are responsible for saving and loading to the database and also for any domain logic that acts on the data. The data structure of the Active Record should exactly match that of the database: one field in the class for each column in the table.
+
+The Active Record class typically has methods that do the following:
+
+* Construct an instances of an Active Record class from a SQL result
+* Construct a new class instance for insertion into the table
+* Get and set column values
+* Wrap business logic where appropriate
+* Update existing objects and update the related rows in the database
+
+Mapping Your Database
+---------------------
+
+### Plural tables, singular classes ###
+
+### Schema lives in the database ###
+
+Creating Records
+----------------
+
+### Using save ###
+
+### Using create ###
+
+Retrieving Existing Rows
+------------------------
+
+### Using find ###
+
+### Using find_by_* ###
+
+Editing and Updating Rows
+-------------------------
+
+### Editing an instance
+
+### Using update_all/update_attributes ###
+
+Deleting Data
+-------------
+
+### Destroying a record ###
+
+### Deleting a record ### \ No newline at end of file
diff --git a/railties/doc/guides/creating_plugins/acts_as_yaffle.txt b/railties/doc/guides/creating_plugins/acts_as_yaffle.txt
new file mode 100644
index 0000000000..12d40deb18
--- /dev/null
+++ b/railties/doc/guides/creating_plugins/acts_as_yaffle.txt
@@ -0,0 +1,193 @@
+== Add an `acts_as_yaffle` method to ActiveRecord ==
+
+A common pattern in plugins is to add a method called `acts_as_something` to models. In this case, you want to write a method called `acts_as_yaffle` that adds a `squawk` method to your models.
+
+To keep things clean, create a new test file called 'acts_as_yaffle_test.rb' in your plugin's test directory and require your test helper.
+
+[source, ruby]
+------------------------------------------------------
+# File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb
+
+require File.dirname(__FILE__) + '/test_helper.rb'
+
+class Hickwall < ActiveRecord::Base
+ acts_as_yaffle
+end
+
+class ActsAsYaffleTest < Test::Unit::TestCase
+end
+------------------------------------------------------
+
+[source, ruby]
+------------------------------------------------------
+# File: vendor/plugins/lib/acts_as_yaffle.rb
+
+module Yaffle
+end
+------------------------------------------------------
+
+One of the most common plugin patterns for `acts_as_yaffle` plugins is to structure your file like so:
+
+[source, ruby]
+------------------------------------------------------
+module Yaffle
+ def self.included(base)
+ base.send :extend, ClassMethods
+ end
+
+ module ClassMethods
+ # any method placed here will apply to classes, like Hickwall
+ def acts_as_something
+ send :include, InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ # any method placed here will apply to instaces, like @hickwall
+ end
+end
+------------------------------------------------------
+
+With structure you can easily separate the methods that will be used for the class (like `Hickwall.some_method`) and the instance (like `@hickwell.some_method`).
+
+Let's add class method named `acts_as_yaffle` - testing it out first. You already defined the ActiveRecord models in your test helper, so if you run tests now they will fail.
+
+Back in your `acts_as_yaffle` file, update ClassMethods like so:
+
+[source, ruby]
+------------------------------------------------------
+module ClassMethods
+ def acts_as_yaffle(options = {})
+ send :include, InstanceMethods
+ end
+end
+------------------------------------------------------
+
+Now that test should pass. Since your plugin is going to work with field names, you need to allow people to define the field names, in case there is a naming conflict. You can write a few simple tests for this:
+
+[source, ruby]
+------------------------------------------------------
+# File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb
+
+require File.dirname(__FILE__) + '/test_helper.rb'
+
+class ActsAsYaffleTest < Test::Unit::TestCase
+ def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
+ assert_equal "last_squawk", Hickwall.yaffle_text_field
+ end
+
+ def test_a_hickwalls_yaffle_date_field_should_be_last_squawked_at
+ assert_equal "last_squawked_at", Hickwall.yaffle_date_field
+ end
+
+ def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
+ assert_equal "last_tweet", Wickwall.yaffle_text_field
+ end
+
+ def test_a_wickwalls_yaffle_date_field_should_be_last_tweeted_at
+ assert_equal "last_tweeted_at", Wickwall.yaffle_date_field
+ end
+end
+------------------------------------------------------
+
+To make these tests pass, you could modify your `acts_as_yaffle` file like so:
+
+[source, ruby]
+------------------------------------------------------
+# File: vendor/plugins/yaffle/lib/acts_as_yaffle.rb
+
+module Yaffle
+ def self.included(base)
+ base.send :extend, ClassMethods
+ end
+
+ module ClassMethods
+ def acts_as_yaffle(options = {})
+ cattr_accessor :yaffle_text_field, :yaffle_date_field
+ self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
+ self.yaffle_date_field = (options[:yaffle_date_field] || :last_squawked_at).to_s
+ send :include, InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ end
+end
+------------------------------------------------------
+
+Now you can add tests for the instance methods, and the instance method itself:
+
+[source, ruby]
+------------------------------------------------------
+# File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb
+
+require File.dirname(__FILE__) + '/test_helper.rb'
+
+class ActsAsYaffleTest < Test::Unit::TestCase
+
+ def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
+ assert_equal "last_squawk", Hickwall.yaffle_text_field
+ end
+ def test_a_hickwalls_yaffle_date_field_should_be_last_squawked_at
+ assert_equal "last_squawked_at", Hickwall.yaffle_date_field
+ end
+
+ def test_a_wickwalls_yaffle_text_field_should_be_last_squawk
+ assert_equal "last_tweet", Wickwall.yaffle_text_field
+ end
+ def test_a_wickwalls_yaffle_date_field_should_be_last_squawked_at
+ assert_equal "last_tweeted_at", Wickwall.yaffle_date_field
+ end
+
+ def test_hickwalls_squawk_should_populate_last_squawk
+ hickwall = Hickwall.new
+ hickwall.squawk("Hello World")
+ assert_equal "squawk! Hello World", hickwall.last_squawk
+ end
+ def test_hickwalls_squawk_should_populate_last_squawked_at
+ hickwall = Hickwall.new
+ hickwall.squawk("Hello World")
+ assert_equal Date.today, hickwall.last_squawked_at
+ end
+
+ def test_wickwalls_squawk_should_populate_last_tweet
+ wickwall = Wickwall.new
+ wickwall.squawk("Hello World")
+ assert_equal "squawk! Hello World", wickwall.last_tweet
+ end
+ def test_wickwalls_squawk_should_populate_last_tweeted_at
+ wickwall = Wickwall.new
+ wickwall.squawk("Hello World")
+ assert_equal Date.today, wickwall.last_tweeted_at
+ end
+end
+------------------------------------------------------
+
+[source, ruby]
+------------------------------------------------------
+# File: vendor/plugins/yaffle/lib/acts_as_yaffle.rb
+
+module Yaffle
+ def self.included(base)
+ base.send :extend, ClassMethods
+ end
+
+ module ClassMethods
+ def acts_as_yaffle(options = {})
+ cattr_accessor :yaffle_text_field, :yaffle_date_field
+ self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
+ self.yaffle_date_field = (options[:yaffle_date_field] || :last_squawked_at).to_s
+ send :include, InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def squawk(string)
+ write_attribute(self.class.yaffle_text_field, string.to_squawk)
+ write_attribute(self.class.yaffle_date_field, Date.today)
+ end
+ end
+end
+------------------------------------------------------
+
+Note the use of `write_attribute` to write to the field in model.
diff --git a/railties/doc/guides/creating_plugins/appendix.txt b/railties/doc/guides/creating_plugins/appendix.txt
new file mode 100644
index 0000000000..a78890ccd5
--- /dev/null
+++ b/railties/doc/guides/creating_plugins/appendix.txt
@@ -0,0 +1,46 @@
+== Appendix ==
+
+=== References ===
+
+ * http://nubyonrails.com/articles/the-complete-guide-to-rails-plugins-part-i
+ * http://nubyonrails.com/articles/2006/05/09/the-complete-guide-to-rails-plugins-part-ii
+ * http://github.com/technoweenie/attachment_fu/tree/master
+ * http://daddy.platte.name/2007/05/rails-plugins-keep-initrb-thin.html
+
+=== Final plugin directory structure ===
+
+The final plugin should have a directory structure that looks something like this:
+
+------------------------------------------------
+ |-- MIT-LICENSE
+ |-- README
+ |-- Rakefile
+ |-- generators
+ | `-- yaffle
+ | |-- USAGE
+ | |-- templates
+ | | `-- definition.txt
+ | `-- yaffle_generator.rb
+ |-- init.rb
+ |-- install.rb
+ |-- lib
+ | |-- acts_as_yaffle.rb
+ | |-- commands.rb
+ | |-- core_ext.rb
+ | |-- routing.rb
+ | `-- view_helpers.rb
+ |-- tasks
+ | `-- yaffle_tasks.rake
+ |-- test
+ | |-- acts_as_yaffle_test.rb
+ | |-- core_ext_test.rb
+ | |-- database.yml
+ | |-- debug.log
+ | |-- routing_test.rb
+ | |-- schema.rb
+ | |-- test_helper.rb
+ | `-- view_helpers_test.rb
+ |-- uninstall.rb
+ `-- yaffle_plugin.sqlite3.db
+------------------------------------------------
+
diff --git a/railties/doc/guides/creating_plugins/basics.markdown b/railties/doc/guides/creating_plugins/basics.markdown
new file mode 100644
index 0000000000..f59e8728d7
--- /dev/null
+++ b/railties/doc/guides/creating_plugins/basics.markdown
@@ -0,0 +1,861 @@
+Creating Plugin Basics
+====================
+
+Pretend for a moment that you are an avid bird watcher. Your favorite bird is the Yaffle, and you want to create a plugin that allows other developers to share in the Yaffle goodness.
+
+In this tutorial you will learn how to create a plugin that includes:
+
+Core Extensions - extending String:
+
+ # Anywhere
+ "hello".squawk # => "squawk! hello! squawk!"
+
+An `acts_as_yaffle` method for Active Record models that adds a "squawk" method:
+
+ class Hickwall < ActiveRecord::Base
+ acts_as_yaffle :yaffle_text_field => :last_sang_at
+ end
+
+ Hickwall.new.squawk("Hello World")
+
+A view helper that will print out squawking info:
+
+ squawk_info_for(@hickwall)
+
+A generator that creates a migration to add squawk columns to a model:
+
+ script/generate yaffle hickwall
+
+A custom generator command:
+
+ class YaffleGenerator < Rails::Generator::NamedBase
+ def manifest
+ m.yaffle_definition
+ end
+ end
+ end
+
+A custom route method:
+
+ ActionController::Routing::Routes.draw do |map|
+ map.yaffles
+ end
+
+In addition you'll learn how to:
+
+* test your plugins
+* work with init.rb, how to store model, views, controllers, helpers and even other plugins in your plugins
+* create documentation for your plugin.
+* write custom rake tasks in your plugin
+
+Create the basic app
+---------------------
+
+In this tutorial we will create a basic rails application with 1 resource: bird. Start out by building the basic rails app:
+
+> The following instructions will work for sqlite3. For more detailed instructions on how to create a rails app for other databases see the API docs.
+
+ rails plugin_demo
+ cd plugin_demo
+ script/generate scaffold bird name:string
+ rake db:migrate
+ script/server
+
+Then navigate to [http://localhost:3000/birds](http://localhost:3000/birds). Make sure you have a functioning rails app before continuing.
+
+Create the plugin
+-----------------------
+
+The built-in Rails plugin generator stubs out a new plugin. Pass the plugin name, either CamelCased or under_scored, as an argument. Pass --with-generator to add an example generator also.
+
+This creates a plugin in vendor/plugins including an init.rb and README as well as standard lib, task, and test directories.
+
+Examples:
+
+ ./script/generate plugin BrowserFilters
+ ./script/generate plugin BrowserFilters --with-generator
+
+Later in the plugin we will create a generator, so go ahead and add the --with-generator option now:
+
+ script/generate plugin yaffle --with-generator
+
+You should see the following output:
+
+ create vendor/plugins/yaffle/lib
+ create vendor/plugins/yaffle/tasks
+ create vendor/plugins/yaffle/test
+ create vendor/plugins/yaffle/README
+ create vendor/plugins/yaffle/MIT-LICENSE
+ create vendor/plugins/yaffle/Rakefile
+ create vendor/plugins/yaffle/init.rb
+ create vendor/plugins/yaffle/install.rb
+ create vendor/plugins/yaffle/uninstall.rb
+ create vendor/plugins/yaffle/lib/yaffle.rb
+ create vendor/plugins/yaffle/tasks/yaffle_tasks.rake
+ create vendor/plugins/yaffle/test/core_ext_test.rb
+ create vendor/plugins/yaffle/generators
+ create vendor/plugins/yaffle/generators/yaffle
+ create vendor/plugins/yaffle/generators/yaffle/templates
+ create vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb
+ create vendor/plugins/yaffle/generators/yaffle/USAGE
+
+For this plugin you won't need the file vendor/plugins/yaffle/lib/yaffle.rb so you can delete that.
+
+ rm vendor/plugins/yaffle/lib/yaffle.rb
+
+> Editor's note: many plugin authors prefer to keep this file, and add all of the require statements in it. That way, they only line in init.rb would be `require "yaffle"`
+> If you are developing a plugin that has a lot of files in the lib directory, you may want to create a subdirectory like lib/yaffle and store your files in there. That way your init.rb file stays clean
+
+Setup the plugin for testing
+------------------------
+
+Testing plugins that use the entire Rails stack can be complex, and the generator doesn't offer any help. In this tutorial you will learn how to test your plugin against multiple different adapters using ActiveRecord. This tutorial will not cover how to use fixtures in plugin tests.
+
+To setup your plugin to allow for easy testing you'll need to add 3 files:
+
+* A database.yml file with all of your connection strings
+* A schema.rb file with your table definitions
+* A test helper that sets up the database before your tests
+
+For this plugin you'll need 2 tables/models, Hickwalls and Wickwalls, so add the following files:
+
+ # File: vendor/plugins/yaffle/test/database.yml
+
+ sqlite:
+ :adapter: sqlite
+ :dbfile: yaffle_plugin.sqlite.db
+ sqlite3:
+ :adapter: sqlite3
+ :dbfile: yaffle_plugin.sqlite3.db
+ postgresql:
+ :adapter: postgresql
+ :username: postgres
+ :password: postgres
+ :database: yaffle_plugin_test
+ :min_messages: ERROR
+ mysql:
+ :adapter: mysql
+ :host: localhost
+ :username: rails
+ :password:
+ :database: yaffle_plugin_test
+
+ # File: vendor/plugins/yaffle/test/test_helper.rb
+
+ ActiveRecord::Schema.define(:version => 0) do
+ create_table :hickwalls, :force => true do |t|
+ t.string :name
+ t.string :last_squawk
+ t.datetime :last_squawked_at
+ end
+ create_table :wickwalls, :force => true do |t|
+ t.string :name
+ t.string :last_tweet
+ t.datetime :last_tweeted_at
+ end
+ end
+
+ # File: vendor/plugins/yaffle/test/test_helper.rb
+
+ ENV['RAILS_ENV'] = 'test'
+ ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '/../../../..'
+
+ require 'test/unit'
+ require File.expand_path(File.join(ENV['RAILS_ROOT'], 'config/environment.rb'))
+
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
+
+ db_adapter = ENV['DB']
+
+ # no db passed, try one of these fine config-free DBs before bombing.
+ db_adapter ||=
+ begin
+ require 'rubygems'
+ require 'sqlite'
+ 'sqlite'
+ rescue MissingSourceFile
+ begin
+ require 'sqlite3'
+ 'sqlite3'
+ rescue MissingSourceFile
+ end
+ end
+
+ if db_adapter.nil?
+ raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
+ end
+
+ ActiveRecord::Base.establish_connection(config[db_adapter])
+
+ load(File.dirname(__FILE__) + "/schema.rb")
+
+ require File.dirname(__FILE__) + '/../init.rb'
+
+ class Hickwall < ActiveRecord::Base
+ acts_as_yaffle
+ end
+
+ class Wickwall < ActiveRecord::Base
+ acts_as_yaffle :yaffle_text_field => :last_tweet, :yaffle_date_field => :last_tweeted_at
+ end
+
+Add a `to_squawk` method to String
+-----------------------
+
+To update a core class you will have to:
+
+* Write tests for the desired functionality
+* Create a file for the code you wish to use
+* Require that file from your init.rb
+
+Most plugins store their code classes in the plugin's lib directory. When you add a file to the lib directory, you must also require that file from init.rb. The file you are going to add for this tutorial is `lib/core_ext.rb`
+
+First, you need to write the tests. Testing plugins is very similar to testing rails apps. The generated test file should look something like this:
+
+ # File: vendor/plugins/yaffle/test/core_ext_test.rb
+
+ require 'test/unit'
+
+ class CoreExtTest < Test::Unit::TestCase
+ # Replace this with your real tests.
+ def test_this_plugin
+ flunk
+ end
+ end
+
+Start off by removing the default test, and adding a require statement for your test helper.
+
+ # File: vendor/plugins/yaffle/test/core_ext_test.rb
+
+ require 'test/unit'
+ require File.dirname(__FILE__) + '/test_helper.rb'
+
+ class CoreExtTest < Test::Unit::TestCase
+ end
+
+Navigate to your plugin directory and run `rake test`
+
+ cd vendor/plugins/yaffle
+ rake test
+
+Your test should fail with `no such file to load -- ./test/../lib/core_ext.rb (LoadError)` because we haven't created any file yet. Create the file `lib/core_ext.rb` and re-run the tests. You should see a different error message:
+
+ 1.) Failure ...
+ No tests were specified
+
+Great - now you are ready to start development. The first thing we'll do is to add a method to String called `to_squawk` which will prefix the string with the word "squawk! ". The test will look something like this:
+
+ # File: vendor/plugins/yaffle/init.rb
+
+ class CoreExtTest < Test::Unit::TestCase
+ def test_string_should_respond_to_squawk
+ assert_equal true, "".respond_to?(:to_squawk)
+ end
+ def test_string_prepend_empty_strings_with_the_word_squawk
+ assert_equal "squawk!", "".to_squawk
+ end
+ def test_string_prepend_non_empty_strings_with_the_word_squawk
+ assert_equal "squawk! Hello World", "Hello World".to_squawk
+ end
+ end
+
+ # File: vendor/plugins/yaffle/init.rb
+
+ require "core_ext"
+
+ # File: vendor/plugins/yaffle/lib/core_ext.rb
+
+ String.class_eval do
+ def to_squawk
+ "squawk! #{self}".strip
+ end
+ end
+
+When monkey-patching existing classes it's often better to use `class_eval` instead of opening the class directly.
+
+To test that your method does what it says it does, run the unit tests. To test this manually, fire up a console and start squawking:
+
+ script/console
+ >> "Hello World".to_squawk
+ => "squawk! Hello World"
+
+If that worked, congratulations! You just created your first test-driven plugin that extends a core ruby class.
+
+Add an `acts_as_yaffle` method to ActiveRecord
+-----------------------
+
+A common pattern in plugins is to add a method called `acts_as_something` to models. In this case, you want to write a method called `acts_as_yaffle` that adds a squawk method to your models.
+
+To keep things clean, create a new test file called `acts_as_yaffle_test.rb` in your plugin's test directory and require your test helper.
+
+ # File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb
+
+ require File.dirname(__FILE__) + '/test_helper.rb'
+
+ class Hickwall < ActiveRecord::Base
+ acts_as_yaffle
+ end
+
+ class ActsAsYaffleTest < Test::Unit::TestCase
+ end
+
+ # File: vendor/plugins/lib/acts_as_yaffle.rb
+
+ module Yaffle
+ end
+
+One of the most common plugin patterns for `acts_as_yaffle` plugins is to structure your file like so:
+
+ module Yaffle
+ def self.included(base)
+ base.send :extend, ClassMethods
+ end
+
+ module ClassMethods
+ # any method placed here will apply to classes, like Hickwall
+ def acts_as_something
+ send :include, InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ # any method placed here will apply to instaces, like @hickwall
+ end
+ end
+
+With structure you can easily separate the methods that will be used for the class (like `Hickwall.some_method`) and the instance (like `@hickwell.some_method`).
+
+Let's add class method named `acts_as_yaffle` - testing it out first. You already defined the ActiveRecord models in your test helper, so if you run tests now they will fail.
+
+Back in your `acts_as_yaffle` file, update ClassMethods like so:
+
+ module ClassMethods
+ def acts_as_yaffle(options = {})
+ send :include, InstanceMethods
+ end
+ end
+
+Now that test should pass. Since your plugin is going to work with field names, you need to allow people to define the field names, in case there is a naming conflict. You can write a few simple tests for this:
+
+ # File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb
+
+ require File.dirname(__FILE__) + '/test_helper.rb'
+
+ class ActsAsYaffleTest < Test::Unit::TestCase
+ def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
+ assert_equal "last_squawk", Hickwall.yaffle_text_field
+ end
+ def test_a_hickwalls_yaffle_date_field_should_be_last_squawked_at
+ assert_equal "last_squawked_at", Hickwall.yaffle_date_field
+ end
+ def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
+ assert_equal "last_tweet", Wickwall.yaffle_text_field
+ end
+ def test_a_wickwalls_yaffle_date_field_should_be_last_tweeted_at
+ assert_equal "last_tweeted_at", Wickwall.yaffle_date_field
+ end
+ end
+
+To make these tests pass, you could modify your `acts_as_yaffle` file like so:
+
+ # File: vendor/plugins/yaffle/lib/acts_as_yaffle.rb
+
+ module Yaffle
+ def self.included(base)
+ base.send :extend, ClassMethods
+ end
+
+ module ClassMethods
+ def acts_as_yaffle(options = {})
+ cattr_accessor :yaffle_text_field, :yaffle_date_field
+ self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
+ self.yaffle_date_field = (options[:yaffle_date_field] || :last_squawked_at).to_s
+ send :include, InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ end
+ end
+
+Now you can add tests for the instance methods, and the instance method itself:
+
+ # File: vendor/plugins/yaffle/test/acts_as_yaffle_test.rb
+
+ require File.dirname(__FILE__) + '/test_helper.rb'
+
+ class ActsAsYaffleTest < Test::Unit::TestCase
+
+ def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
+ assert_equal "last_squawk", Hickwall.yaffle_text_field
+ end
+ def test_a_hickwalls_yaffle_date_field_should_be_last_squawked_at
+ assert_equal "last_squawked_at", Hickwall.yaffle_date_field
+ end
+
+ def test_a_wickwalls_yaffle_text_field_should_be_last_squawk
+ assert_equal "last_tweet", Wickwall.yaffle_text_field
+ end
+ def test_a_wickwalls_yaffle_date_field_should_be_last_squawked_at
+ assert_equal "last_tweeted_at", Wickwall.yaffle_date_field
+ end
+
+ def test_hickwalls_squawk_should_populate_last_squawk
+ hickwall = Hickwall.new
+ hickwall.squawk("Hello World")
+ assert_equal "squawk! Hello World", hickwall.last_squawk
+ end
+ def test_hickwalls_squawk_should_populate_last_squawked_at
+ hickwall = Hickwall.new
+ hickwall.squawk("Hello World")
+ assert_equal Date.today, hickwall.last_squawked_at
+ end
+
+ def test_wickwalls_squawk_should_populate_last_tweet
+ wickwall = Wickwall.new
+ wickwall.squawk("Hello World")
+ assert_equal "squawk! Hello World", wickwall.last_tweet
+ end
+ def test_wickwalls_squawk_should_populate_last_tweeted_at
+ wickwall = Wickwall.new
+ wickwall.squawk("Hello World")
+ assert_equal Date.today, wickwall.last_tweeted_at
+ end
+ end
+
+ # File: vendor/plugins/yaffle/lib/acts_as_yaffle.rb
+
+ module Yaffle
+ def self.included(base)
+ base.send :extend, ClassMethods
+ end
+
+ module ClassMethods
+ def acts_as_yaffle(options = {})
+ cattr_accessor :yaffle_text_field, :yaffle_date_field
+ self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
+ self.yaffle_date_field = (options[:yaffle_date_field] || :last_squawked_at).to_s
+ send :include, InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def squawk(string)
+ write_attribute(self.class.yaffle_text_field, string.to_squawk)
+ write_attribute(self.class.yaffle_date_field, Date.today)
+ end
+ end
+ end
+
+Note the use of write_attribute to write to the field in model.
+
+Create a view helper
+-----------------------
+
+Creating a view helper is a 3-step process:
+
+* Add an appropriately named file to the lib directory
+* Require the file and hooks in init.rb
+* Write the tests
+
+First, create the test to define the functionality you want:
+
+ # File: vendor/plugins/yaffle/test/view_helpers_test.rb
+
+ require File.dirname(__FILE__) + '/test_helper.rb'
+ include YaffleViewHelper
+
+ class ViewHelpersTest < Test::Unit::TestCase
+ def test_squawk_info_for_should_return_the_text_and_date
+ time = Time.now
+ hickwall = Hickwall.new
+ hickwall.last_squawk = "Hello World"
+ hickwall.last_squawked_at = time
+ assert_equal "Hello World, #{time.to_s}", squawk_info_for(hickwall)
+ end
+ end
+
+Then add the following statements to init.rb:
+
+ # File: vendor/plugins/yaffle/init.rb
+
+ require "view_helpers"
+ ActionView::Base.send :include, YaffleViewHelper
+
+Then add the view helpers file and
+
+ # File: vendor/plugins/yaffle/lib/view_helpers.rb
+
+ module YaffleViewHelper
+ def squawk_info_for(yaffle)
+ returning "" do |result|
+ result << yaffle.read_attribute(yaffle.class.yaffle_text_field)
+ result << ", "
+ result << yaffle.read_attribute(yaffle.class.yaffle_date_field).to_s
+ end
+ end
+ end
+
+You can also test this in script/console by using the "helper" method:
+
+ script/console
+ >> helper.squawk_info_for(@some_yaffle_instance)
+
+Create a migration generator
+-----------------------
+
+When you created the plugin above, you specified the --with-generator option, so you already have the generator stubs in your plugin.
+
+We'll be relying on the built-in rails generate template for this tutorial. Going into the details of generators is beyond the scope of this tutorial.
+
+Type:
+
+ script/generate
+
+You should see the line:
+
+ Plugins (vendor/plugins): yaffle
+
+When you run `script/generate yaffle` you should see the contents of your USAGE file. For this plugin, the USAGE file looks like this:
+
+ Description:
+ Creates a migration that adds yaffle squawk fields to the given model
+
+ Example:
+ ./script/generate yaffle hickwall
+
+ This will create:
+ db/migrate/TIMESTAMP_add_yaffle_fields_to_hickwall
+
+Now you can add code to your generator:
+
+ # File: vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb
+
+ class YaffleGenerator < Rails::Generator::NamedBase
+ def manifest
+ record do |m|
+ m.migration_template 'migration:migration.rb', "db/migrate", {:assigns => yaffle_local_assigns,
+ :migration_file_name => "add_yaffle_fields_to_#{custom_file_name}"
+ }
+ end
+ end
+
+ private
+ def custom_file_name
+ custom_name = class_name.underscore.downcase
+ custom_name = custom_name.pluralize if ActiveRecord::Base.pluralize_table_names
+ end
+
+ def yaffle_local_assigns
+ returning(assigns = {}) do
+ assigns[:migration_action] = "add"
+ assigns[:class_name] = "add_yaffle_fields_to_#{custom_file_name}"
+ assigns[:table_name] = custom_file_name
+ assigns[:attributes] = [Rails::Generator::GeneratedAttribute.new("last_squawk", "string")]
+ assigns[:attributes] << Rails::Generator::GeneratedAttribute.new("last_squawked_at", "datetime")
+ end
+ end
+ end
+
+Note that you need to be aware of whether or not table names are pluralized.
+
+This does a few things:
+
+* Reuses the built in rails migration_template method
+* Reuses the built-in rails migration template
+
+When you run the generator like
+
+ script/generate yaffle bird
+
+You will see a new file:
+
+ # File: db/migrate/20080529225649_add_yaffle_fields_to_birds.rb
+
+ class AddYaffleFieldsToBirds < ActiveRecord::Migration
+ def self.up
+ add_column :birds, :last_squawk, :string
+ add_column :birds, :last_squawked_at, :datetime
+ end
+
+ def self.down
+ remove_column :birds, :last_squawked_at
+ remove_column :birds, :last_squawk
+ end
+ end
+
+Add a custom generator command
+------------------------
+
+You may have noticed above that you can used one of the built-in rails migration commands `m.migration_template`. You can create your own commands for these, using the following steps:
+
+1. Add the require and hook statements to init.rb
+2. Create the commands - creating 3 sets, Create, Destroy, List
+3. Add the method to your generator
+
+Working with the internals of generators is beyond the scope of this tutorial, but here is a basic example:
+
+ # File: vendor/plugins/yaffle/init.rb
+
+ require "commands"
+ Rails::Generator::Commands::Create.send :include, Yaffle::Generator::Commands::Create
+ Rails::Generator::Commands::Destroy.send :include, Yaffle::Generator::Commands::Destroy
+ Rails::Generator::Commands::List.send :include, Yaffle::Generator::Commands::List
+
+ # File: vendor/plugins/yaffle/lib/commands.rb
+
+ require 'rails_generator'
+ require 'rails_generator/commands'
+
+ module Yaffle #:nodoc:
+ module Generator #:nodoc:
+ module Commands #:nodoc:
+ module Create
+ def yaffle_definition
+ file("definition.txt", "definition.txt")
+ end
+ end
+
+ module Destroy
+ def yaffle_definition
+ file("definition.txt", "definition.txt")
+ end
+ end
+
+ module List
+ def yaffle_definition
+ file("definition.txt", "definition.txt")
+ end
+ end
+ end
+ end
+ end
+
+ # File: vendor/plugins/yaffle/generators/yaffle/templates/definition.txt
+
+ Yaffle is a bird
+
+ # File: vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb
+
+ class YaffleGenerator < Rails::Generator::NamedBase
+ def manifest
+ m.yaffle_definition
+ end
+ end
+ end
+
+This example just uses the built-in "file" method, but you could do anything that ruby allows.
+
+Add a Custom Route
+------------------------
+
+Testing routes in plugins can be complex, especially if the controllers are also in the plugin itself. Jamis Buck showed a great example of this in [http://weblog.jamisbuck.org/2006/10/26/monkey-patching-rails-extending-routes-2](http://weblog.jamisbuck.org/2006/10/26/monkey-patching-rails-extending-routes-2)
+
+ # File: vendor/plugins/yaffle/test/routing_test.rb
+
+ require "#{File.dirname(__FILE__)}/test_helper"
+
+ class RoutingTest < Test::Unit::TestCase
+
+ def setup
+ ActionController::Routing::Routes.draw do |map|
+ map.yaffles
+ end
+ end
+
+ def test_yaffles_route
+ assert_recognition :get, "/yaffles", :controller => "yaffles_controller", :action => "index"
+ end
+
+ private
+
+ # yes, I know about assert_recognizes, but it has proven problematic to
+ # use in these tests, since it uses RouteSet#recognize (which actually
+ # tries to instantiate the controller) and because it uses an awkward
+ # parameter order.
+ def assert_recognition(method, path, options)
+ result = ActionController::Routing::Routes.recognize_path(path, :method => method)
+ assert_equal options, result
+ end
+ end
+
+ # File: vendor/plugins/yaffle/init.rb
+
+ require "routing"
+ ActionController::Routing::RouteSet::Mapper.send :include, Yaffle::Routing::MapperExtensions
+
+ # File: vendor/plugins/yaffle/lib/routing.rb
+
+ module Yaffle #:nodoc:
+ module Routing #:nodoc:
+ module MapperExtensions
+ def yaffles
+ @set.add_route("/yaffles", {:controller => "yaffles_controller", :action => "index"})
+ end
+ end
+ end
+ end
+
+ # File: config/routes.rb
+
+ ActionController::Routing::Routes.draw do |map|
+ ...
+ map.yaffles
+ end
+
+You can also see if your routes work by running `rake routes` from your app directory.
+
+Generate RDoc Documentation
+-----------------------
+
+Once your plugin is stable, the tests pass on all database and you are ready to deploy do everyone else a favor and document it! Luckily, writing documentation for your plugin is easy.
+
+The first step is to update the README file with detailed information about how to use your plugin. A few key things to include are:
+
+* Your name
+* How to install
+* How to add the functionality to the app (several examples of common use cases)
+* Warning, gotchas or tips that might help save users time
+
+Once your README is solid, go through and add rdoc comments to all of the methods that developers will use.
+
+Before you generate your documentation, be sure to go through and add nodoc comments to those modules and methods that are not important to your users.
+
+Once your comments are good to go, navigate to your plugin directory and run
+
+ rake rdoc
+
+Work with init.rb
+------------------------
+
+The plugin initializer script init.rb is invoked via `eval` (not require) so it has slightly different behavior.
+
+If you reopen any classes in init.rb itself your changes will potentially be made to the wrong module. There are 2 ways around this:
+
+The first way is to explicitly define the top-level module space for all modules and classes, like ::Hash
+
+ # File: vendor/plugins/yaffle/init.rb
+
+ class ::Hash
+ def is_a_special_hash?
+ true
+ end
+ end
+
+OR you can use `module_eval` or `class_eval`
+
+ # File: vendor/plugins/yaffle/init.rb
+
+ Hash.class_eval do
+ def is_a_special_hash?
+ true
+ end
+ end
+
+Store models, views, helpers, and controllers in your plugins
+------------------------
+
+You can easily store models, views, helpers and controllers in plugins. Just create a folder for each in the lib folder, add them to the load path and remove them from the load once path:
+
+ # File: vendor/plugins/yaffle/init.rb
+
+ %w{ models controllers helpers }.each do |dir|
+ path = File.join(directory, 'lib', dir)
+ $LOAD_PATH << path
+ Dependencies.load_paths << path
+ Dependencies.load_once_paths.delete(path)
+ end
+
+Adding directories to the load path makes them appear just like files in the the main app directory - except that they are only loaded once, so you have to restart the web server to see the changes in the browser.
+
+Adding directories to the load once paths allow those changes to picked up as soon as you save the file - without having to restart the web server.
+
+Write custom rake tasks in your plugin
+-------------------------
+
+When you created the plugin with the built-in rails generator, it generated a rake file for you in `vendor/plugins/yaffle/tasks/yaffle.rake`. Any rake task you add here will be available to the app.
+
+Many plugin authors put all of their rake tasks into a common namespace that is the same as the plugin, like so:
+
+ # File: vendor/plugins/yaffle/tasks/yaffle.rake
+
+ namespace :yaffle do
+ desc "Prints out the word 'Yaffle'"
+ task :squawk => :environment do
+ puts "squawk!"
+ end
+ end
+
+When you run `rake -T` from your plugin you will see
+
+ yaffle:squawk "Prints out..."
+
+You can add as many files as you want in the tasks directory, and if they end in .rake Rails will pick them up.
+
+Store plugins in alternate locations
+-------------------------
+
+You can store plugins wherever you want - you just have to add those plugins to the plugins path in environment.rb
+
+Since the plugin is only loaded after the plugin paths are defined, you can't redefine this in your plugins - but it may be good to now.
+
+You can even store plugins inside of other plugins for complete plugin madness!
+
+ config.plugin_paths << File.join(RAILS_ROOT,"vendor","plugins","yaffle","lib","plugins")
+
+Create your own Plugin Loaders and Plugin Locators
+------------------------
+
+If the built-in plugin behavior is inadequate, you can change almost every aspect of the location and loading process. You can write your own plugin locators and plugin loaders, but that's beyond the scope of this tutorial.
+
+Use Custom Plugin Generators
+------------------------
+
+If you are an RSpec fan, you can install the `rspec_plugin_generator`, which will generate the spec folder and database for you.
+
+[http://github.com/pat-maddox/rspec-plugin-generator/tree/master](http://github.com/pat-maddox/rspec-plugin-generator/tree/master)
+
+References
+------------------------
+
+* [http://nubyonrails.com/articles/the-complete-guide-to-rails-plugins-part-i](http://nubyonrails.com/articles/the-complete-guide-to-rails-plugins-part-i)
+* [http://nubyonrails.com/articles/2006/05/09/the-complete-guide-to-rails-plugins-part-ii](http://nubyonrails.com/articles/2006/05/09/the-complete-guide-to-rails-plugins-part-ii)
+* [http://github.com/technoweenie/attachment_fu/tree/master](http://github.com/technoweenie/attachment_fu/tree/master)
+* [http://daddy.platte.name/2007/05/rails-plugins-keep-initrb-thin.html](http://daddy.platte.name/2007/05/rails-plugins-keep-initrb-thin.html)
+
+Appendices
+------------------------
+
+The final plugin should have a directory structure that looks something like this:
+
+ |-- MIT-LICENSE
+ |-- README
+ |-- Rakefile
+ |-- generators
+ | `-- yaffle
+ | |-- USAGE
+ | |-- templates
+ | | `-- definition.txt
+ | `-- yaffle_generator.rb
+ |-- init.rb
+ |-- install.rb
+ |-- lib
+ | |-- acts_as_yaffle.rb
+ | |-- commands.rb
+ | |-- core_ext.rb
+ | |-- routing.rb
+ | `-- view_helpers.rb
+ |-- tasks
+ | `-- yaffle_tasks.rake
+ |-- test
+ | |-- acts_as_yaffle_test.rb
+ | |-- core_ext_test.rb
+ | |-- database.yml
+ | |-- debug.log
+ | |-- routing_test.rb
+ | |-- schema.rb
+ | |-- test_helper.rb
+ | `-- view_helpers_test.rb
+ |-- uninstall.rb
+ `-- yaffle_plugin.sqlite3.db
diff --git a/railties/doc/guides/creating_plugins/creating_plugins.txt b/railties/doc/guides/creating_plugins/creating_plugins.txt
new file mode 100644
index 0000000000..f2ed6ed8bb
--- /dev/null
+++ b/railties/doc/guides/creating_plugins/creating_plugins.txt
@@ -0,0 +1,84 @@
+The Basics of Creating Rails Plugins
+====================================
+
+Pretend for a moment that you are an avid bird watcher. Your favorite bird is the Yaffle, and you want to create a plugin that allows other developers to share in the Yaffle goodness.
+
+In this tutorial you will learn how to create a plugin that includes:
+
+ * Core Extensions - extending String with a `to_squawk` method:
++
+[source, ruby]
+-------------------------------------------
+# Anywhere
+"hello!".to_squawk # => "squawk! hello!"
+-------------------------------------------
+
+* An `acts_as_yaffle` method for ActiveRecord models that adds a `squawk` method:
++
+[source, ruby]
+-------------------------------------------
+class Hickwall < ActiveRecord::Base
+ acts_as_yaffle :yaffle_text_field => :last_sang_at
+end
+
+Hickwall.new.squawk("Hello World")
+-------------------------------------------
+
+* A view helper that will print out squawking info:
++
+[source, ruby]
+-------------------------------------------
+squawk_info_for(@hickwall)
+-------------------------------------------
+
+* A generator that creates a migration to add squawk columns to a model:
++
+-------------------------------------------
+script/generate yaffle hickwall
+-------------------------------------------
+
+* A custom generator command:
++
+[source, ruby]
+-------------------------------------------
+class YaffleGenerator < Rails::Generator::NamedBase
+ def manifest
+ m.yaffle_definition
+ end
+end
+-------------------------------------------
+
+* A custom route method:
++
+[source, ruby]
+-------------------------------------------
+ActionController::Routing::Routes.draw do |map|
+ map.yaffles
+end
+-------------------------------------------
+
+In addition you'll learn how to:
+
+ * test your plugins.
+ * work with 'init.rb', how to store model, views, controllers, helpers and even other plugins in your plugins.
+ * create documentation for your plugin.
+ * write custom Rake tasks in your plugin.
+
+
+include::preparation.txt[]
+
+include::string_to_squawk.txt[]
+
+include::acts_as_yaffle.txt[]
+
+include::view_helper.txt[]
+
+include::migration_generator.txt[]
+
+include::custom_generator.txt[]
+
+include::custom_route.txt[]
+
+include::odds_and_ends.txt[]
+
+include::appendix.txt[]
diff --git a/railties/doc/guides/creating_plugins/custom_generator.txt b/railties/doc/guides/creating_plugins/custom_generator.txt
new file mode 100644
index 0000000000..6d9613ea01
--- /dev/null
+++ b/railties/doc/guides/creating_plugins/custom_generator.txt
@@ -0,0 +1,69 @@
+== Add a custom generator command ==
+
+You may have noticed above that you can used one of the built-in rails migration commands `m.migration_template`. You can create your own commands for these, using the following steps:
+
+ 1. Add the require and hook statements to init.rb.
+ 2. Create the commands - creating 3 sets, Create, Destroy, List.
+ 3. Add the method to your generator.
+
+Working with the internals of generators is beyond the scope of this tutorial, but here is a basic example:
+
+[source, ruby]
+-----------------------------------------------------------
+# File: vendor/plugins/yaffle/init.rb
+require "commands"
+Rails::Generator::Commands::Create.send :include, Yaffle::Generator::Commands::Create
+Rails::Generator::Commands::Destroy.send :include, Yaffle::Generator::Commands::Destroy
+Rails::Generator::Commands::List.send :include, Yaffle::Generator::Commands::List
+-----------------------------------------------------------
+
+[source, ruby]
+-----------------------------------------------------------
+# File: vendor/plugins/yaffle/lib/commands.rb
+
+require 'rails_generator'
+require 'rails_generator/commands'
+
+module Yaffle #:nodoc:
+ module Generator #:nodoc:
+ module Commands #:nodoc:
+ module Create
+ def yaffle_definition
+ file("definition.txt", "definition.txt")
+ end
+ end
+
+ module Destroy
+ def yaffle_definition
+ file("definition.txt", "definition.txt")
+ end
+ end
+
+ module List
+ def yaffle_definition
+ file("definition.txt", "definition.txt")
+ end
+ end
+ end
+ end
+end
+-----------------------------------------------------------
+
+-----------------------------------------------------------
+# File: vendor/plugins/yaffle/generators/yaffle/templates/definition.txt
+
+Yaffle is a bird
+-----------------------------------------------------------
+
+[source, ruby]
+-----------------------------------------------------------
+# File: vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb
+
+class YaffleGenerator < Rails::Generator::NamedBase
+ def manifest
+ m.yaffle_definition
+ end
+end
+-----------------------------------------------------------
+
+This example just uses the built-in "file" method, but you could do anything that Ruby allows.
diff --git a/railties/doc/guides/creating_plugins/custom_route.txt b/railties/doc/guides/creating_plugins/custom_route.txt
new file mode 100644
index 0000000000..7e399247ee
--- /dev/null
+++ b/railties/doc/guides/creating_plugins/custom_route.txt
@@ -0,0 +1,69 @@
+== Add a Custom Route ==
+
+Testing routes in plugins can be complex, especially if the controllers are also in the plugin itself. Jamis Buck showed a great example of this in http://weblog.jamisbuck.org/2006/10/26/monkey-patching-rails-extending-routes-2.
+
+[source, ruby]
+--------------------------------------------------------
+# File: vendor/plugins/yaffle/test/routing_test.rb
+
+require "#{File.dirname(__FILE__)}/test_helper"
+
+class RoutingTest < Test::Unit::TestCase
+
+ def setup
+ ActionController::Routing::Routes.draw do |map|
+ map.yaffles
+ end
+ end
+
+ def test_yaffles_route
+ assert_recognition :get, "/yaffles", :controller => "yaffles_controller", :action => "index"
+ end
+
+ private
+
+ # yes, I know about assert_recognizes, but it has proven problematic to
+ # use in these tests, since it uses RouteSet#recognize (which actually
+ # tries to instantiate the controller) and because it uses an awkward
+ # parameter order.
+ def assert_recognition(method, path, options)
+ result = ActionController::Routing::Routes.recognize_path(path, :method => method)
+ assert_equal options, result
+ end
+end
+--------------------------------------------------------
+
+[source, ruby]
+--------------------------------------------------------
+# File: vendor/plugins/yaffle/init.rb
+
+require "routing"
+ActionController::Routing::RouteSet::Mapper.send :include, Yaffle::Routing::MapperExtensions
+--------------------------------------------------------
+
+[source, ruby]
+--------------------------------------------------------
+# File: vendor/plugins/yaffle/lib/routing.rb
+
+module Yaffle #:nodoc:
+ module Routing #:nodoc:
+ module MapperExtensions
+ def yaffles
+ @set.add_route("/yaffles", {:controller => "yaffles_controller", :action => "index"})
+ end
+ end
+ end
+end
+--------------------------------------------------------
+
+[source, ruby]
+--------------------------------------------------------
+# File: config/routes.rb
+
+ActionController::Routing::Routes.draw do |map|
+ ...
+ map.yaffles
+end
+--------------------------------------------------------
+
+You can also see if your routes work by running `rake routes` from your app directory.
diff --git a/railties/doc/guides/creating_plugins/migration_generator.txt b/railties/doc/guides/creating_plugins/migration_generator.txt
new file mode 100644
index 0000000000..598a0c8437
--- /dev/null
+++ b/railties/doc/guides/creating_plugins/migration_generator.txt
@@ -0,0 +1,89 @@
+== Create a migration generator ==
+
+When you created the plugin above, you specified the --with-generator option, so you already have the generator stubs in your plugin.
+
+We'll be relying on the built-in rails generate template for this tutorial. Going into the details of generators is beyond the scope of this tutorial.
+
+Type:
+
+ script/generate
+
+You should see the line:
+
+ Plugins (vendor/plugins): yaffle
+
+When you run `script/generate yaffle` you should see the contents of your USAGE file. For this plugin, the USAGE file looks like this:
+
+------------------------------------------------------------------
+Description:
+ Creates a migration that adds yaffle squawk fields to the given model
+
+Example:
+ ./script/generate yaffle hickwall
+
+ This will create:
+ db/migrate/TIMESTAMP_add_yaffle_fields_to_hickwall
+------------------------------------------------------------------
+
+Now you can add code to your generator:
+
+[source, ruby]
+------------------------------------------------------------------
+# File: vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb
+
+class YaffleGenerator < Rails::Generator::NamedBase
+ def manifest
+ record do |m|
+ m.migration_template 'migration:migration.rb', "db/migrate", {:assigns => yaffle_local_assigns,
+ :migration_file_name => "add_yaffle_fields_to_#{custom_file_name}"
+ }
+ end
+ end
+
+ private
+ def custom_file_name
+ custom_name = class_name.underscore.downcase
+ custom_name = custom_name.pluralize if ActiveRecord::Base.pluralize_table_names
+ end
+
+ def yaffle_local_assigns
+ returning(assigns = {}) do
+ assigns[:migration_action] = "add"
+ assigns[:class_name] = "add_yaffle_fields_to_#{custom_file_name}"
+ assigns[:table_name] = custom_file_name
+ assigns[:attributes] = [Rails::Generator::GeneratedAttribute.new("last_squawk", "string")]
+ assigns[:attributes] << Rails::Generator::GeneratedAttribute.new("last_squawked_at", "datetime")
+ end
+ end
+end
+------------------------------------------------------------------
+
+Note that you need to be aware of whether or not table names are pluralized.
+
+This does a few things:
+
+ * Reuses the built in rails `migration_template` method.
+ * Reuses the built-in rails migration template.
+
+When you run the generator like
+
+ script/generate yaffle bird
+
+You will see a new file:
+
+[source, ruby]
+------------------------------------------------------------------
+# File: db/migrate/20080529225649_add_yaffle_fields_to_birds.rb
+
+class AddYaffleFieldsToBirds < ActiveRecord::Migration
+ def self.up
+ add_column :birds, :last_squawk, :string
+ add_column :birds, :last_squawked_at, :datetime
+ end
+
+ def self.down
+ remove_column :birds, :last_squawked_at
+ remove_column :birds, :last_squawk
+ end
+end
+------------------------------------------------------------------
diff --git a/railties/doc/guides/creating_plugins/odds_and_ends.txt b/railties/doc/guides/creating_plugins/odds_and_ends.txt
new file mode 100644
index 0000000000..eb127f73ca
--- /dev/null
+++ b/railties/doc/guides/creating_plugins/odds_and_ends.txt
@@ -0,0 +1,122 @@
+== Odds and ends ==
+
+=== Work with init.rb ===
+
+The plugin initializer script 'init.rb' is invoked via `eval` (not `require`) so it has slightly different behavior.
+
+If you reopen any classes in init.rb itself your changes will potentially be made to the wrong module. There are 2 ways around this:
+
+The first way is to explicitly define the top-level module space for all modules and classes, like `::Hash`:
+
+[source, ruby]
+---------------------------------------------------
+# File: vendor/plugins/yaffle/init.rb
+
+class ::Hash
+ def is_a_special_hash?
+ true
+ end
+end
+---------------------------------------------------
+
+OR you can use `module_eval` or `class_eval`:
+
+---------------------------------------------------
+# File: vendor/plugins/yaffle/init.rb
+
+Hash.class_eval do
+ def is_a_special_hash?
+ true
+ end
+end
+---------------------------------------------------
+
+=== Generate RDoc Documentation ===
+
+Once your plugin is stable, the tests pass on all database and you are ready to deploy do everyone else a favor and document it! Luckily, writing documentation for your plugin is easy.
+
+The first step is to update the README file with detailed information about how to use your plugin. A few key things to include are:
+
+ * Your name.
+ * How to install.
+ * How to add the functionality to the app (several examples of common use cases).
+ * Warning, gotchas or tips that might help save users time.
+
+Once your README is solid, go through and add rdoc comments to all of the methods that developers will use.
+
+Before you generate your documentation, be sure to go through and add nodoc comments to those modules and methods that are not important to your users.
+
+Once your comments are good to go, navigate to your plugin directory and run:
+
+ rake rdoc
+
+
+=== Store models, views, helpers, and controllers in your plugins ===
+
+You can easily store models, views, helpers and controllers in plugins. Just create a folder for each in the lib folder, add them to the load path and remove them from the load once path:
+
+[source, ruby]
+---------------------------------------------------------
+# File: vendor/plugins/yaffle/init.rb
+
+%w{ models controllers helpers }.each do |dir|
+ path = File.join(directory, 'lib', dir)
+ $LOAD_PATH << path
+ Dependencies.load_paths << path
+ Dependencies.load_once_paths.delete(path)
+end
+---------------------------------------------------------
+
+Adding directories to the load path makes them appear just like files in the the main app directory - except that they are only loaded once, so you have to restart the web server to see the changes in the browser.
+
+Adding directories to the load once paths allow those changes to picked up as soon as you save the file - without having to restart the web server.
+
+
+=== Write custom Rake tasks in your plugin ===
+
+When you created the plugin with the built-in rails generator, it generated a rake file for you in 'vendor/plugins/yaffle/tasks/yaffle.rake'. Any rake task you add here will be available to the app.
+
+Many plugin authors put all of their rake tasks into a common namespace that is the same as the plugin, like so:
+
+[source, ruby]
+---------------------------------------------------------
+# File: vendor/plugins/yaffle/tasks/yaffle.rake
+
+namespace :yaffle do
+ desc "Prints out the word 'Yaffle'"
+ task :squawk => :environment do
+ puts "squawk!"
+ end
+end
+---------------------------------------------------------
+
+When you run `rake -T` from your plugin you will see:
+
+---------------------------------------------------------
+yaffle:squawk # Prints out the word 'Yaffle'
+---------------------------------------------------------
+
+You can add as many files as you want in the tasks directory, and if they end in .rake Rails will pick them up.
+
+=== Store plugins in alternate locations ===
+
+You can store plugins wherever you want - you just have to add those plugins to the plugins path in 'environment.rb'.
+
+Since the plugin is only loaded after the plugin paths are defined, you can't redefine this in your plugins - but it may be good to now.
+
+You can even store plugins inside of other plugins for complete plugin madness!
+
+[source, ruby]
+---------------------------------------------------------
+config.plugin_paths << File.join(RAILS_ROOT,"vendor","plugins","yaffle","lib","plugins")
+---------------------------------------------------------
+
+=== Create your own Plugin Loaders and Plugin Locators ===
+
+If the built-in plugin behavior is inadequate, you can change almost every aspect of the location and loading process. You can write your own plugin locators and plugin loaders, but that's beyond the scope of this tutorial.
+
+
+=== Use Custom Plugin Generators ===
+
+If you are an RSpec fan, you can install the `rspec_plugin_generator` gem, which will generate the spec folder and database for you. See http://github.com/pat-maddox/rspec-plugin-generator/tree/master.
+
diff --git a/railties/doc/guides/creating_plugins/preparation.txt b/railties/doc/guides/creating_plugins/preparation.txt
new file mode 100644
index 0000000000..77e3a3561f
--- /dev/null
+++ b/railties/doc/guides/creating_plugins/preparation.txt
@@ -0,0 +1,169 @@
+== Preparation ==
+
+=== Create the basic app ===
+
+In this tutorial we will create a basic rails application with 1 resource: bird. Start out by building the basic rails app:
+
+------------------------------------------------
+rails plugin_demo
+cd plugin_demo
+script/generate scaffold bird name:string
+rake db:migrate
+script/server
+------------------------------------------------
+
+Then navigate to http://localhost:3000/birds. Make sure you have a functioning rails app before continuing.
+
+NOTE: The aforementioned instructions will work for sqlite3. For more detailed instructions on how to create a rails app for other databases see the API docs.
+
+
+=== Create the plugin ===
+
+The built-in Rails plugin generator stubs out a new plugin. Pass the plugin name, either 'CamelCased' or 'under_scored', as an argument. Pass `\--with-generator` to add an example generator also.
+
+This creates a plugin in 'vendor/plugins' including an 'init.rb' and 'README' as well as standard 'lib', 'task', and 'test' directories.
+
+Examples:
+----------------------------------------------
+./script/generate plugin BrowserFilters
+./script/generate plugin BrowserFilters --with-generator
+----------------------------------------------
+
+Later in the plugin we will create a generator, so go ahead and add the `\--with-generator` option now:
+
+----------------------------------------------
+script/generate plugin yaffle --with-generator
+----------------------------------------------
+
+You should see the following output:
+
+----------------------------------------------
+create vendor/plugins/yaffle/lib
+create vendor/plugins/yaffle/tasks
+create vendor/plugins/yaffle/test
+create vendor/plugins/yaffle/README
+create vendor/plugins/yaffle/MIT-LICENSE
+create vendor/plugins/yaffle/Rakefile
+create vendor/plugins/yaffle/init.rb
+create vendor/plugins/yaffle/install.rb
+create vendor/plugins/yaffle/uninstall.rb
+create vendor/plugins/yaffle/lib/yaffle.rb
+create vendor/plugins/yaffle/tasks/yaffle_tasks.rake
+create vendor/plugins/yaffle/test/core_ext_test.rb
+create vendor/plugins/yaffle/generators
+create vendor/plugins/yaffle/generators/yaffle
+create vendor/plugins/yaffle/generators/yaffle/templates
+create vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb
+create vendor/plugins/yaffle/generators/yaffle/USAGE
+----------------------------------------------
+
+For this plugin you won't need the file 'vendor/plugins/yaffle/lib/yaffle.rb' so you can delete that.
+
+----------------------------------------------
+rm vendor/plugins/yaffle/lib/yaffle.rb
+----------------------------------------------
+
+.Editor's note:
+NOTE: Many plugin authors prefer to keep this file, and add all of the require statements in it. That way, they only line in init.rb would be `require "yaffle"`. If you are developing a plugin that has a lot of files in the lib directory, you may want to create a subdirectory like lib/yaffle and store your files in there. That way your init.rb file stays clean
+
+
+=== Setup the plugin for testing ===
+
+Testing plugins that use the entire Rails stack can be complex, and the generator doesn't offer any help. In this tutorial you will learn how to test your plugin against multiple different adapters using ActiveRecord. This tutorial will not cover how to use fixtures in plugin tests.
+
+To setup your plugin to allow for easy testing you'll need to add 3 files:
+
+ * A 'database.yml' file with all of your connection strings.
+ * A 'schema.rb' file with your table definitions.
+ * A test helper that sets up the database before your tests.
+
+For this plugin you'll need 2 tables/models, Hickwalls and Wickwalls, so add the following files:
+
+*vendor/plugins/yaffle/test/database.yml:*
+
+----------------------------------------------
+sqlite:
+ :adapter: sqlite
+ :dbfile: yaffle_plugin.sqlite.db
+
+sqlite3:
+ :adapter: sqlite3
+ :dbfile: yaffle_plugin.sqlite3.db
+
+postgresql:
+ :adapter: postgresql
+ :username: postgres
+ :password: postgres
+ :database: yaffle_plugin_test
+ :min_messages: ERROR
+
+mysql:
+ :adapter: mysql
+ :host: localhost
+ :username: rails
+ :password:
+ :database: yaffle_plugin_test
+----------------------------------------------
+
+*vendor/plugins/yaffle/test/test_helper.rb:*
+
+[source, ruby]
+----------------------------------------------
+ActiveRecord::Schema.define(:version => 0) do
+ create_table :hickwalls, :force => true do |t|
+ t.string :name
+ t.string :last_squawk
+ t.datetime :last_squawked_at
+ end
+ create_table :wickwalls, :force => true do |t|
+ t.string :name
+ t.string :last_tweet
+ t.datetime :last_tweeted_at
+ end
+end
+
+# File: vendor/plugins/yaffle/test/test_helper.rb
+
+ENV['RAILS_ENV'] = 'test'
+ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '/../../../..'
+
+require 'test/unit'
+require File.expand_path(File.join(ENV['RAILS_ROOT'], 'config/environment.rb'))
+
+config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
+ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
+
+db_adapter = ENV['DB']
+
+# no db passed, try one of these fine config-free DBs before bombing.
+db_adapter ||=
+ begin
+ require 'rubygems'
+ require 'sqlite'
+ 'sqlite'
+ rescue MissingSourceFile
+ begin
+ require 'sqlite3'
+ 'sqlite3'
+ rescue MissingSourceFile
+ end
+ end
+
+if db_adapter.nil?
+ raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
+end
+
+ActiveRecord::Base.establish_connection(config[db_adapter])
+
+load(File.dirname(__FILE__) + "/schema.rb")
+
+require File.dirname(__FILE__) + '/../init.rb'
+
+class Hickwall < ActiveRecord::Base
+ acts_as_yaffle
+end
+
+class Wickwall < ActiveRecord::Base
+ acts_as_yaffle :yaffle_text_field => :last_tweet, :yaffle_date_field => :last_tweeted_at
+end
+----------------------------------------------
diff --git a/railties/doc/guides/creating_plugins/string_to_squawk.txt b/railties/doc/guides/creating_plugins/string_to_squawk.txt
new file mode 100644
index 0000000000..50516cef69
--- /dev/null
+++ b/railties/doc/guides/creating_plugins/string_to_squawk.txt
@@ -0,0 +1,103 @@
+== Add a `to_squawk` method to String ==
+
+To update a core class you will have to:
+
+ * Write tests for the desired functionality.
+ * Create a file for the code you wish to use.
+ * Require that file from your 'init.rb'.
+
+Most plugins store their code classes in the plugin's lib directory. When you add a file to the lib directory, you must also require that file from 'init.rb'. The file you are going to add for this tutorial is 'lib/core_ext.rb'.
+
+First, you need to write the tests. Testing plugins is very similar to testing rails apps. The generated test file should look something like this:
+
+[source, ruby]
+--------------------------------------------------------
+# File: vendor/plugins/yaffle/test/core_ext_test.rb
+
+require 'test/unit'
+
+class CoreExtTest < Test::Unit::TestCase
+ # Replace this with your real tests.
+ def test_this_plugin
+ flunk
+ end
+end
+--------------------------------------------------------
+
+Start off by removing the default test, and adding a require statement for your test helper.
+
+[source, ruby]
+--------------------------------------------------------
+# File: vendor/plugins/yaffle/test/core_ext_test.rb
+
+require 'test/unit'
+require File.dirname(__FILE__) + '/test_helper.rb'
+
+class CoreExtTest < Test::Unit::TestCase
+end
+--------------------------------------------------------
+
+Navigate to your plugin directory and run `rake test`:
+
+--------------------------------------------------------
+cd vendor/plugins/yaffle
+rake test
+--------------------------------------------------------
+
+Your test should fail with `no such file to load -- ./test/../lib/core_ext.rb (LoadError)` because we haven't created any file yet. Create the file 'lib/core_ext.rb' and re-run the tests. You should see a different error message:
+
+--------------------------------------------------------
+1.) Failure ...
+No tests were specified
+--------------------------------------------------------
+
+Great - now you are ready to start development. The first thing we'll do is to add a method to String called `to_squawk` which will prefix the string with the word ``squawk!''. The test will look something like this:
+
+[source, ruby]
+--------------------------------------------------------
+# File: vendor/plugins/yaffle/init.rb
+
+class CoreExtTest < Test::Unit::TestCase
+ def test_string_should_respond_to_squawk
+ assert_equal true, "".respond_to?(:to_squawk)
+ end
+
+ def test_string_prepend_empty_strings_with_the_word_squawk
+ assert_equal "squawk!", "".to_squawk
+ end
+
+ def test_string_prepend_non_empty_strings_with_the_word_squawk
+ assert_equal "squawk! Hello World", "Hello World".to_squawk
+ end
+end
+--------------------------------------------------------
+
+[source, ruby]
+--------------------------------------------------------
+# File: vendor/plugins/yaffle/init.rb
+
+require "core_ext"
+--------------------------------------------------------
+
+[source, ruby]
+--------------------------------------------------------
+# File: vendor/plugins/yaffle/lib/core_ext.rb
+
+String.class_eval do
+ def to_squawk
+ "squawk! #{self}".strip
+ end
+end
+--------------------------------------------------------
+
+When monkey-patching existing classes it's often better to use `class_eval` instead of opening the class directly.
+
+To test that your method does what it says it does, run the unit tests. To test this manually, fire up a console and start squawking:
+
+--------------------------------------------------------
+$ ./script/console
+>> "Hello World".to_squawk
+=> "squawk! Hello World"
+--------------------------------------------------------
+
+If that worked, congratulations! You just created your first test-driven plugin that extends a core ruby class.
diff --git a/railties/doc/guides/creating_plugins/view_helper.txt b/railties/doc/guides/creating_plugins/view_helper.txt
new file mode 100644
index 0000000000..b03a190e1a
--- /dev/null
+++ b/railties/doc/guides/creating_plugins/view_helper.txt
@@ -0,0 +1,61 @@
+== Create a `squawk_info_for` view helper ==
+
+Creating a view helper is a 3-step process:
+
+ * Add an appropriately named file to the 'lib' directory.
+ * Require the file and hooks in 'init.rb'.
+ * Write the tests.
+
+First, create the test to define the functionality you want:
+
+[source, ruby]
+---------------------------------------------------------------
+# File: vendor/plugins/yaffle/test/view_helpers_test.rb
+
+require File.dirname(__FILE__) + '/test_helper.rb'
+include YaffleViewHelper
+
+class ViewHelpersTest < Test::Unit::TestCase
+ def test_squawk_info_for_should_return_the_text_and_date
+ time = Time.now
+ hickwall = Hickwall.new
+ hickwall.last_squawk = "Hello World"
+ hickwall.last_squawked_at = time
+ assert_equal "Hello World, #{time.to_s}", squawk_info_for(hickwall)
+ end
+end
+---------------------------------------------------------------
+
+Then add the following statements to init.rb:
+
+[source, ruby]
+---------------------------------------------------------------
+# File: vendor/plugins/yaffle/init.rb
+
+require "view_helpers"
+ActionView::Base.send :include, YaffleViewHelper
+---------------------------------------------------------------
+
+Then add the view helpers file and
+
+[source, ruby]
+---------------------------------------------------------------
+# File: vendor/plugins/yaffle/lib/view_helpers.rb
+
+module YaffleViewHelper
+ def squawk_info_for(yaffle)
+ returning "" do |result|
+ result << yaffle.read_attribute(yaffle.class.yaffle_text_field)
+ result << ", "
+ result << yaffle.read_attribute(yaffle.class.yaffle_date_field).to_s
+ end
+ end
+end
+---------------------------------------------------------------
+
+You can also test this in script/console by using the `helper` method:
+
+---------------------------------------------------------------
+$ ./script/console
+>> helper.squawk_info_for(@some_yaffle_instance)
+---------------------------------------------------------------
diff --git a/railties/doc/guides/icons/README b/railties/doc/guides/icons/README
new file mode 100644
index 0000000000..f12b2a730c
--- /dev/null
+++ b/railties/doc/guides/icons/README
@@ -0,0 +1,5 @@
+Replaced the plain DocBook XSL admonition icons with Jimmac's DocBook
+icons (http://jimmac.musichall.cz/ikony.php3). I dropped transparency
+from the Jimmac icons to get round MS IE and FOP PNG incompatibilies.
+
+Stuart Rackham
diff --git a/railties/doc/guides/icons/callouts/1.png b/railties/doc/guides/icons/callouts/1.png
new file mode 100644
index 0000000000..7d473430b7
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/1.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/10.png b/railties/doc/guides/icons/callouts/10.png
new file mode 100644
index 0000000000..997bbc8246
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/10.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/11.png b/railties/doc/guides/icons/callouts/11.png
new file mode 100644
index 0000000000..ce47dac3f5
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/11.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/12.png b/railties/doc/guides/icons/callouts/12.png
new file mode 100644
index 0000000000..31daf4e2f2
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/12.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/13.png b/railties/doc/guides/icons/callouts/13.png
new file mode 100644
index 0000000000..14021a89c2
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/13.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/14.png b/railties/doc/guides/icons/callouts/14.png
new file mode 100644
index 0000000000..64014b75fe
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/14.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/15.png b/railties/doc/guides/icons/callouts/15.png
new file mode 100644
index 0000000000..0d65765fcf
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/15.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/2.png b/railties/doc/guides/icons/callouts/2.png
new file mode 100644
index 0000000000..5d09341b2f
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/2.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/3.png b/railties/doc/guides/icons/callouts/3.png
new file mode 100644
index 0000000000..ef7b700471
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/3.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/4.png b/railties/doc/guides/icons/callouts/4.png
new file mode 100644
index 0000000000..adb8364eb5
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/4.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/5.png b/railties/doc/guides/icons/callouts/5.png
new file mode 100644
index 0000000000..4d7eb46002
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/5.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/6.png b/railties/doc/guides/icons/callouts/6.png
new file mode 100644
index 0000000000..0ba694af6c
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/6.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/7.png b/railties/doc/guides/icons/callouts/7.png
new file mode 100644
index 0000000000..472e96f8ac
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/7.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/8.png b/railties/doc/guides/icons/callouts/8.png
new file mode 100644
index 0000000000..5e60973c21
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/8.png
Binary files differ
diff --git a/railties/doc/guides/icons/callouts/9.png b/railties/doc/guides/icons/callouts/9.png
new file mode 100644
index 0000000000..a0676d26cc
--- /dev/null
+++ b/railties/doc/guides/icons/callouts/9.png
Binary files differ
diff --git a/railties/doc/guides/icons/caution.png b/railties/doc/guides/icons/caution.png
new file mode 100644
index 0000000000..cb9d5ea0df
--- /dev/null
+++ b/railties/doc/guides/icons/caution.png
Binary files differ
diff --git a/railties/doc/guides/icons/example.png b/railties/doc/guides/icons/example.png
new file mode 100644
index 0000000000..bba1c0010d
--- /dev/null
+++ b/railties/doc/guides/icons/example.png
Binary files differ
diff --git a/railties/doc/guides/icons/home.png b/railties/doc/guides/icons/home.png
new file mode 100644
index 0000000000..37a5231bac
--- /dev/null
+++ b/railties/doc/guides/icons/home.png
Binary files differ
diff --git a/railties/doc/guides/icons/important.png b/railties/doc/guides/icons/important.png
new file mode 100644
index 0000000000..1096c23295
--- /dev/null
+++ b/railties/doc/guides/icons/important.png
Binary files differ
diff --git a/railties/doc/guides/icons/next.png b/railties/doc/guides/icons/next.png
new file mode 100644
index 0000000000..64e126bdda
--- /dev/null
+++ b/railties/doc/guides/icons/next.png
Binary files differ
diff --git a/railties/doc/guides/icons/note.png b/railties/doc/guides/icons/note.png
new file mode 100644
index 0000000000..841820f7c4
--- /dev/null
+++ b/railties/doc/guides/icons/note.png
Binary files differ
diff --git a/railties/doc/guides/icons/prev.png b/railties/doc/guides/icons/prev.png
new file mode 100644
index 0000000000..3e8f12fe24
--- /dev/null
+++ b/railties/doc/guides/icons/prev.png
Binary files differ
diff --git a/railties/doc/guides/icons/tip.png b/railties/doc/guides/icons/tip.png
new file mode 100644
index 0000000000..a3a029d898
--- /dev/null
+++ b/railties/doc/guides/icons/tip.png
Binary files differ
diff --git a/railties/doc/guides/icons/up.png b/railties/doc/guides/icons/up.png
new file mode 100644
index 0000000000..2db1ce62fa
--- /dev/null
+++ b/railties/doc/guides/icons/up.png
Binary files differ
diff --git a/railties/doc/guides/icons/warning.png b/railties/doc/guides/icons/warning.png
new file mode 100644
index 0000000000..0b0c419df2
--- /dev/null
+++ b/railties/doc/guides/icons/warning.png
Binary files differ
diff --git a/railties/doc/guides/securing_rails_applications/creating_records_directly_from_form_parameters.txt b/railties/doc/guides/securing_rails_applications/creating_records_directly_from_form_parameters.txt
new file mode 100644
index 0000000000..b237c4a17f
--- /dev/null
+++ b/railties/doc/guides/securing_rails_applications/creating_records_directly_from_form_parameters.txt
@@ -0,0 +1,86 @@
+== Creating records directly from form parameters ==
+
+=== The problem ===
+
+Let's say you want to make a user registration system. Your users table looks like this:
+
+-------------------------------------------------------
+CREATE TABLE users (
+ id INTEGER PRIMARY KEY,
+ name VARCHAR(20) NOT NULL, -- the login name
+ password VARCHAR(20) NOT NULL,
+ role VARCHAR(20) NOT NULL DEFAULT "User", -- e.g. "Admin", "Moderator, "User"
+ approved INTEGER NOT NULL DEFAULT 0 -- is the registered accound approved by the administrator?
+);
+
+CREATE UNIQUE INDEX users_name_unique ON users(name);
+-------------------------------------------------------
+
+The registration form:
+
+[source, xhtml]
+-------------------------------------------------------
+<form method="post" action="http://website.domain/user/register">
+ <input type="text" name="user[name]" />
+ <input type="text" name="user[password]" />
+</form>
+-------------------------------------------------------
+
+The easiest way to create a user object from the form data in the controller is:
+
+[source, ruby]
+-------------------------------------------------------
+User.create(params[:user])
+-------------------------------------------------------
+
+But what happens if someone decides to save the registration form to his disk and play around with adding a few fields?
+
+[source, xhtml]
+-------------------------------------------------------
+<form method="post" action="http://website.domain/user/register">
+ <input type="text" name="user[name]" />
+ <input type="text" name="user[password]" />
+ <input type="text" name="user[role]" value="Admin" />
+ <input type="text" name="user[approved]" value="1" />
+</form>
+-------------------------------------------------------
+
+He can create an account, make himself admin and approve his own account with one click.
+
+=== The solution ===
+
+Active Record provides two ways of securing sensitive attributes from being overwritten by malicious users that change the form. The first is `attr_protected` that denies mass-assignment the right to change the named parameters.
+
+Using `attr_protected`, we can secure the User models like this:
+
+[source, ruby]
+-------------------------------------------------------
+class User < ActiveRecord::Base
+ attr_protected :approved, :role
+end
+-------------------------------------------------------
+
+This will ensure that on doing `User.create(params[:user])` both `params[:user][:approved]` and `params[:user][:role]` will be ignored. You'll have to manually set them like this:
+
+[source, ruby]
+-------------------------------------------------------
+user = User.new(params[:user])
+user.approved = sanitize_properly(params[:user][:approved])
+user.role = sanitize_properly(params[:user][:role])
+-------------------------------------------------------
+
+=== Allowing instead of protecting ===
+
+If you're afraid you might forget to apply `attr_protected` to the right attributes before making your model available to the cruel world, you can also specify the protection in reverse. You simply allow access instead of deny it, so only the attributes named in `attr_accessible` are available for mass-assignment.
+
+Using `attr_accessible`, we can secure the User models like this:
+
+[source, ruby]
+-------------------------------------------------------
+class User < ActiveRecord::Base
+ attr_accessible :name, :password
+end
+-------------------------------------------------------
+
+This description has exactly the same affect as doing `attr_protected :approved, :role`, but when you add new attrbutes to the User model, they'll be protected by default.
+
diff --git a/railties/doc/guides/securing_rails_applications/cross_site_scripting.txt b/railties/doc/guides/securing_rails_applications/cross_site_scripting.txt
new file mode 100644
index 0000000000..56b3940511
--- /dev/null
+++ b/railties/doc/guides/securing_rails_applications/cross_site_scripting.txt
@@ -0,0 +1,64 @@
+== Cross Site Scripting (CSS/XSS) ==
+
+=== The problem ===
+
+Many web applications use session cookies to track the requests of a user. The cookie is used to identify the request and connect it to the session data (the `session` method in Rails). Usually the session contains a reference to the user that is currently logged in, e.g. the id of a User object.
+
+Cross Site Scripting is a technique for ``stealing'' the cookie from another visitor of the website, and thus stealing the login. Cookies are only available from the domain where the were originally created. The easiest way to get access to the cookie is to place a specially crafted piece of JavaScript code on the website; the script can read the cookie of a visitor and send it to the attacker, e.g. by transmitting the data as an URL parameter to another website.
+2.2 Example of an attack
+
+A site with the following Eruby template is vulnerable to XSS:
+
+------------------------------------
+<%= params[:text] %>
+------------------------------------
+
+The 'text' parameter is directly included in the page, so it is possible to insert JavaScript code by setting the parameter to something like
+
+[source, xhtml]
+------------------------------------
+<script>alert(document.cookie)</script>
+------------------------------------
+
+After url-encoding the script (e.g. with `CGI.encode`) the attacker can put it into an URL and make his victim open the URL in the browser.
+
+If you open the URL
+
+------------------------------------
+http://website.domain/controller/action?text=%3Cscript%3Ealert%28document.cookie%29%3C%2Fscript%3E
+------------------------------------
+
+you will get a popup window with your session cookie. Instead of opening the popup the script could as well send the session id to another server.
+
+=== How to protect your application ===
+
+Obviously the key to a successful attack is the possibility to place JavaScript on a website that can receive the session cookie. This can be a blog with user comments, a web forum or a Wiki. How can you protect against it? Strictly convert HTML meta characters (`<` and `>`) to the equivalent HTML entities (`&lt;` and `&gt;`) in every string that is rendered in the website. This will ensure that, no matter what kind of text an attacker enters in a form or attaches to an URL, the browser will always render it as plain text and never interpret any HTML tags. This is a good idea anyway, as a user can easily mess up your layout by leaving tags open. If you want the user to be able to format his texts, it is usually better to use a markup language like Textile, Markdown or RDoc.
+
+Rails provides the helper method `h` for HTML meta character conversion in Views.
+
+Example:
+
+------------------------------------
+<%=h post.subject %>
+<%=h post.text %>
+------------------------------------
+
+You should accustom to using `h` for any variable that is rendered in the view, even if you think you can trust it to be from a reliable source.
+
+=== XSS attacks using an echo service ===
+
+The echo service is a service running on TCP port 7 that returns back everything you send to it. On Debian it is active by default. Now how can this be a security problem for a web application?
+
+If the server of the target website 'target.domain' is running an echo service, the attacker could create a form like the following on his own website:
+
+[source, xhtml]
+------------------------------------
+<form action="http://target.domain:7/" method="post">
+ <input type="hidden" name="code" value="some_javascript_code_here" />
+ <input type="submit" />
+</form>
+------------------------------------
+
+If he makes someone who has a session cookie on the target website submit this form, the content of the hidden field is sent to the echo server at port 7 – and returned. If the browser decides to display the returned data as HTML (some versions of IE do), it will execute the JavaScript code; and because the domain is 'target.domain' the session cookie is available for the script.
+
+This is rather a client-side issue, but to reduce the probability of a successful attack you should deactivate any echo services on the web server. However this does not provide full security, because there are also some other services (e.g. FTP, POP3) that can be used instead of the echo server.
diff --git a/railties/doc/guides/securing_rails_applications/securing_rails_applications.txt b/railties/doc/guides/securing_rails_applications/securing_rails_applications.txt
new file mode 100644
index 0000000000..b2cebbd311
--- /dev/null
+++ b/railties/doc/guides/securing_rails_applications/securing_rails_applications.txt
@@ -0,0 +1,14 @@
+Securing Rails applications
+===========================
+
+This manual describes common security problems in web applications and how
+to avoid them with Rails. If you have any questions or suggestions, please
+mail me at ror(at)andreas-s.net.
+
+
+include::sql_injection.txt[]
+
+include::cross_site_scripting.txt[]
+
+include::creating_records_directly_from_form_parameters.txt[]
+
diff --git a/railties/doc/guides/securing_rails_applications/sql_injection.txt b/railties/doc/guides/securing_rails_applications/sql_injection.txt
new file mode 100644
index 0000000000..51a0520b39
--- /dev/null
+++ b/railties/doc/guides/securing_rails_applications/sql_injection.txt
@@ -0,0 +1,87 @@
+== SQL Injection ==
+
+=== The problem ===
+
+SQL injection is the #1 security problem in many web applications. How does it work? If the web application includes strings from unreliable sources (usually form parameters) in SQL statements and doesn't correctly quote any SQL meta characters like backslashes or single quotes, an attacker can change `WHERE` conditions in SQL statements, create records with invalid data or even execute arbitrary SQL statements.
+
+=== How to protect your application ===
+
+If you only use the predefined ActiveRecord functions (attributes, save, find) without writing any conditions, limits or SQL queries yourself, ActiveRecord takes care of quoting any dangerous characters in the data for you.
+
+If you don't, you have to make sure that strings for arguments that are directly used to build SQL queries (like the condition, limit and sort arguments for `find :all`) do not contain any SQL meta characters.
+
+=== Using arbitrary strings in conditions and SQL statements ===
+
+==== Wrong ====
+
+Imagine a webmail system where a user can select a list of all the emails with a certain subject. A query could look like this:
+
+[source, ruby]
+---------------------------------------------------------------------------
+Email.find(:all, "owner_id = 123 AND subject = '#{params[:subject]}'")
+---------------------------------------------------------------------------
+
+This is dangerous. Imagine a user sending the string `\' OR 1 \--` in the parameter 'subject'; the resulting statement will look like this:
+
+[source, ruby]
+---------------------------------------------------------------------------
+Email.find(:all, "owner_id = 123 AND subject = '' OR 1 --'")
+---------------------------------------------------------------------------
+
+Because of ``OR 1'' the condition is always true. The part ``\--'' starts a SQL comment; everything after it will be ignored. The result: the user will get a list of all the emails in the database.
+
+(Of course the 'owner_id' would have to be inserted dynamically in a real application; this was omitted to keep the examples as simple as possible.)
+
+==== Correct ====
+
+[source, ruby]
+---------------------------------------------------------------------------
+Email.find(:all, ["owner_id = 123 AND subject = ?", params[:subject]])
+---------------------------------------------------------------------------
+
+If the argument for find_all is an array instead of a string, ActiveRecord will insert the elements 2..n of the array for the `?` placeholders in the element 1, add quotation marks if the elements are strings, and quote all characters that have a special meaning for the database adapter used by the `Email` model.
+
+If you don't like the syntax of the array, you can take care of the quoting yourself by calling the `quote_value` method of the model class. You have to do this when you use `find_by_sql`, as the Array argument doesn't work there:
+
+[source, ruby]
+---------------------------------------------------------------------------
+Email.find_by_sql("SELECT * FROM email WHERE owner_id = 123 AND subject = #{Email.quote_value(subject)}")
+---------------------------------------------------------------------------
+
+The quotation marks are added automatically by `Email.quote_value` if the argument is a string.
+
+=== Extracting queries into model methods ===
+
+If you need to execute a query with the similar options in several places in your code, you should create a model method for that query. Instead of
+
+[source, ruby]
+---------------------------------------------------------------------------
+emails = Email.find(:all, ["subject = ?", subject])
+---------------------------------------------------------------------------
+
+you could define the following class method in the model:
+
+[source, ruby]
+---------------------------------------------------------------------------
+class Email < ActiveRecord::Base
+ def self.find_with_subject(subject)
+ Email.find(:all, ["subject = ?", subject])
+ end
+end
+---------------------------------------------------------------------------
+
+and call it like this:
+
+[source, ruby]
+---------------------------------------------------------------------------
+emails = Email.find_with_subject(subject)
+---------------------------------------------------------------------------
+
+This has the advantage that you don't have to care about meta characters when using the function `find_with_subject`. Generally you should always make sure that this kind of model method can not break anything, even if it is called with untrusted arguments.
+
+.ActiveRecord automatically generates finder methods
+NOTE: The `find_with_subject` method given in the above example is actually redundant.
+ActiveRecord automatically generates finder methods for columns. In this case, ActiveRecord
+automatically generates the method `find_by_subject` (which works exactly like the `find_with_subject`
+method given in the example). `find_with_subject` is only given here as an example. In general,
+you should use ActiveRecord's auto-generated finder methods instead of writing your own.
diff --git a/railties/doc/guides/testing_rails_applications/testing_rails_applications.txt b/railties/doc/guides/testing_rails_applications/testing_rails_applications.txt
new file mode 100644
index 0000000000..a775ed2f09
--- /dev/null
+++ b/railties/doc/guides/testing_rails_applications/testing_rails_applications.txt
@@ -0,0 +1,1371 @@
+A Guide to Testing Rails Applications
+=====================================
+
+This article is for fellow Rubyists looking for more information on test writing and how that fits into Ruby On Rails. If you're new to test writing or experienced with test writing, but not in Rails, hopefully you'll gain some practical tips and insight on successful testing.
+Assumptions
+
+Just so we're all on the same page here, I'm making a few assumptions about you.
+
+ * You've got Ruby installed and know how to run a Ruby script.
+ * You've got Rails installed
+ * You've created a basic Rails application with 1 controller and 1 model.
+
+If you haven't accomplished all of the above, you might be jumping ahead of yourself. Check out www.rubyonrails.org for some great beginner's tutorials.
+
+Make sure you have Ruby 1.8.6 or greater and Rails 2.0 or greater.
+
+Also, hats off to xal (and others) for creating this site.
+
+
+== Testing 1-2-3... is this thing on?
+
+=== What are These Strange Creatures?
+
+I have to say that tests are such a straight-forward concept that it's actually harder to describe what they are than it is to show you how to do them.
+
+So what are tests?
+
+The Official Answer::
+ Tests are collections of questions and scenarios that provide us (developers) with consistent results which prove that our application is doing what we expect it to do.
+
+The Unofficial Answer::
+ We write tests to interrogate, taunt, bully, punish, and otherwise kick the crap out of our application code until it consistently gives us the correct answer. Think of it as ``tough love''. ;)
+
+Need some examples?
+
+ * ensure user names have at least 4 characters
+ * ensure that when we save this record, the administrator is e-mailed
+ * ensure all replies are deleted when we delete the parent message
+ * ensure that Canadians have GST of 7% applied to their purchases
+
+The idea of tests is that you write them at the same time as you write your application. In essence, you're writing two application. As your application grows and becomes more complex, you create many tests. Re-running these tests at any point will help you discover if you've ``broke'' anything along the way.
+
+Some developers actually take this a step further and create the tests before they create the application. This is called Test Driven Development (TDD), and although I personally don't subscribe to it, it's a totally legit way of coding.
+
+There's volumes written about Test Driven Development and testing in general. For more information, talk to Mr. Google.
+
+=== So Why Test?
+
+As mentioned before, tests offer proof that you've done a good job. Your tests become your own personal QA assistant. If you code your tests correctly, at any point in development, you will know:
+
+ * what processes work
+ * what processes fail
+ * the effect of adding new pieces onto your application
+
+With your tests as your safety net, you can do wild refactoring of your code and be able to tell what needs to be addressed simply by seeing which tests fail.
+
+In addition to normal "did it pass?" testing, you can go the opposite route. You can explicitly try to break your application such as feeding it unexpected and crazy input, shutting down critical resources like a database to see what happens, and tampering with things such as session values just to see what happens. By testing your application where the weak points are, you can fix it *before* it ever becomes an issue.
+
+A strong suite of tests ensures your application has a high level of quality. It's worth the extra effort.
+
+Another positive side-effect of writing tests is that you have basic usage documentation of how things work. Simply by looking at your testing code, you can see how you need to work with an object to make it do it's thing.
+
+But enough about theory.
+
+
+== Introducing Test/Unit
+
+=== The Small Picture
+
+==== Hello World
+
+As we all know, Ruby ships with a boat load of great libraries. If you're like me, you've only used a quarter of them or so. One little gem of a library is 'test/unit'. It comes packaged with Ruby 1.8.1, so there's nothing to download.
+
+By using one line of code `require \'test/unit'`, you arm your Ruby script with the capability to write tests.
+
+Let's show you a really basic test, and then we'll step through it and explain some details.
+
+[source,ruby]
+------------------------------------------------------
+# hello_test.rb
+
+require 'test/unit'
+
+class HelloTestCase < Test::Unit::TestCase
+ def test_hello
+ assert true
+ end
+end
+------------------------------------------------------
+
+Quite possibly the simplest and least useful test ever invented, but it shows you the bare bones of writing a test case. That script can be run from the command line the same way your run any other Ruby script.
+
+Simply run `ruby hello_test.rb` and you will see the following:
+
+------------------------------------------------------
+Started
+.
+Finished in 0.0 seconds.
+
+1 tests, 1 assertions, 0 failures, 0 errors
+------------------------------------------------------
+
+Congratulations, your first test.
+
+
+==== What Does It All Mean?
+
+By looking at the output of a test, you will be able to tell if the tests pass or not. In our example, not surprisingly, we've passed. The summary shows *1 test, 1 assertion, 0 failures, and 0 errors*.
+
+So, let's break our test source code down.
+
+First, line 1.
+
+[source,ruby]
+------------------------------------------------------
+require 'test/unit'
+------------------------------------------------------
+
+You'll always have this when writing unit tests. It contains the classes and functionality to write and run unit tests.
+
+Next, we have a class `HelloTestCase` which derives from `Test::Unit::TestCase`.
+
+[source,ruby]
+------------------------------------------------------
+class HelloTestCase < Test::Unit::TestCase
+------------------------------------------------------
+
+All of the tests that you create will directly subclass `Test::Unit::TestCase`. The TestCase provides the ``housing'' of where your tests will live. More on this in a bit.
+
+Finally, we have a method called `test_hello`.
+
+[source,ruby]
+------------------------------------------------------
+def test_hello
+------------------------------------------------------
+
+All tests must follow this naming convention: *their names start with the first 4 characters test*, as in `test_hello`, `testme`, and `testarosa`. If you create a method that doesn't start with test, the testing framework won't recognize it as a test, hence, won't run it automatically, hence it is a normal Ruby method.
+
+Inside our `test_hello` method, we have an assertion.
+
+[source,ruby]
+------------------------------------------------------
+assert true
+------------------------------------------------------
+
+Assertions are where the real work gets done. There are a whole army of different types of assertions that you'll use to make sure your code is doing the right thing.
+
+
+=== The Big Picture
+
+Grab a cup of coffee and dunk your head in some ice water, because here's some more theory.
+
+There are 4 major players in the testing game.
+
+==== A: The Assertion
+
+An 'Assertion' is 1 line of code that evaluates an object (or expression) for expected results.
+
+For example, is this value = that value? is this object nil? does this line of code throw an exception? is the user's password greater than 5 characters?
+
+==== B: The Test ====
+
+A 'Test' is method that contains assertions which represent a particular testing scenario.
+
+For example, we may have a test method called `test_valid_password`. In order for this test to pass, we might need to check a few things:
+
+ * password is 4 or more characters
+ * password isn't the word ‘password'
+ * password isn't all spaces
+
+If all of these assertions are successful, the test will pass.
+
+==== C: The Test Case ====
+A 'Test Case' is a class inherited from `Test::Unit::TestCase` containing a testing ``strategy'' comprised of contextually related tests.
+
+For example, I may have a test case called UserTestCase which has a bunch of tests that check that:
+
+ * the password is valid (`test_password`)
+ * the username doesn't have any forbidden words (`test_username_cussin`)
+ * a user is under the age of 150 (`test_shriveled_raisin`)
+
+==== D: The Test Suite ====
+A 'Test Suite' is a collection of test cases. When you run a test suite, it will, in turn, execute each test that belongs to it.
+
+For example, instead of running each test unit individually, you can run them all by creating a suite and including them. This is good for stuff like continuous-build integration.
+
+We won't get into test suites in this article.
+
+
+==== The Hierarchy ====
+
+The relationship of these objects to one-another looks like this:
+
+ * a test suite
+ * has many test cases
+ * which have many tests
+ * which have many assertions
+
+
+== Hello World on Steroids ==
+
+=== The Victim ===
+
+In the last episode, we learned that we write tests to prove our code works properly. Let's create a really simple class for us to test.
+
+[source,ruby]
+--------------------------------------------------
+# secret_agent.rb
+class SecretAgent
+
+ # simple public properties
+ attr_accessor :username
+ attr_accessor :password
+
+ # our "constructor"
+ def initialize(username,password)
+ @username = username
+ @password = password
+ end
+
+ # the logic to determine if the password is
+ # good enough for the user to use
+ def is_password_secure?
+ return false if @password.nil?
+ return false if @password.empty?
+ return false if @password.size < 4
+ return false if @password 'stirred'
+ return false if @password ‘password'
+ return false if @password == @username
+ true
+ end
+end
+--------------------------------------------------
+
+Ok. So, we've got a class which represents a secret agent. What we're about to do is test a user to make sure that their password is secure enough to use in our top-secret database.
+
+The `is_password_secure?` method will answer that for us. If the user's password passes our stringent set of rules, then the function will return a true value and the secret agent granted access.
+
+
+=== The Assault ===
+
+Let's build upon the last test we wrote and add in a few more things, specifically, let's test out this brand new SecretAgent.
+
+[source,ruby]
+-----------------------------------------------
+require 'test/unit'
+require 'secret_agent'
+
+class HelloTestCase < Test::Unit::TestCase
+
+ def test_hello
+ assert true
+ end
+
+ # our new test will exercise the new SecretAgent class
+ def test_these_passwords
+ # first, let's try a few passwords that should fail
+ assert !SecretAgent.new("bond","abc").is_password_secure?
+ assert !SecretAgent.new("bond","007").is_password_secure?
+ assert !SecretAgent.new("bond","stirred").is_password_secure?
+ assert !SecretAgent.new("bond","password").is_password_secure?
+ assert !SecretAgent.new("bond","bond").is_password_secure?
+ assert !SecretAgent.new("bond","").is_password_secure?
+ assert !SecretAgent.new("bond",nil).is_password_secure?
+
+ # now, let's try passwords that should succeed
+ assert SecretAgent.new("bond","goldfinger").is_password_secure?
+ assert SecretAgent.new("bond","1234").is_password_secure?
+ assert SecretAgent.new("bond","shaken").is_password_secure?
+ end
+end
+-----------------------------------------------
+
+In this example, we've expanded our test case by adding another test called `test_these_passwords` with 10 assertions. Let's run it, cross our fingers, and hope it passes.
+
+-----------------------------------------------
+Started
+..
+Finished in 0.01 seconds.
+
+2 tests, 11 assertions, 0 failures, 0 errors
+-----------------------------------------------
+
+Sweet! It passed!
+
+A couple of new things to notice about the results. See the line right underneath the word Started? Notice it has 2 dots instead of only 1 before? Each . represents a test. You can have 1 of 3 values where the dots are.
+
+ * *.* means successful test (pass)
+ * *F* means failed test
+ * *E* means an error has occurred
+
+=== Failure, Error and General Discomfort ===
+
+Let's add another in 2 more tests. This time, we'll make one of them fail and the other throw an error. Yes, on purpose. Yes, I'm a trained professional.
+
+[source,ruby]
+---------------------------------------------------------------
+...
+# this will result in a failure because the assertion fails... plus everyone
+# knows batman really exists
+def test_bam_zap_sock_pow
+ batman = nil
+ assert_not_nil batman
+end
+
+# this will result in an error because it contains an undefined symbol
+def test_uh_oh_hotdog
+ assert_not_nil does_this_var_speak_korean
+end
+...
+---------------------------------------------------------------
+
+Ok... Let's run this puppy.
+
+---------------------------------------------------------------
+Started
+F..E
+Finished in 0.07 seconds.
+
+1) Failure:
+test_bam_zap_sock_pow(HelloTestCase) [/example/test.rb:62]:
+<nil> expected to not be nil.
+
+2) Error:
+test_uh_oh_hotdog(HelloTestCase):
+NameError: undefined local variable or method 'does_this_var_speak_korean' for
+#<HelloTestCase:0x28c59a0> /example/test.rb:68:in 'test_uh_oh_hotdog'
+
+4 tests, 12 assertions, 1 failures, 1 errors
+---------------------------------------------------------------
+
+Wow. Nasty.
+
+Take a look at the 2nd line. This time we have 'F..E' which means failure, pass, pass, error. In the details underneath the total elapsed time, you see what exactly went wrong and where.
+
+The incredibly observant will notice that even though the two new methods were added as the 3rd and 4th tests, they showed up in the results as 1st and 4th. That's because the tests are sorted alphabetically.
+
+So, that's what errors and failures look like. The difference is, a failure represents an assertion attempt that gave us the wrong results whereas an error is a Ruby problem.
+
+
+=== This Side Up ^ ===
+
+Another thing about assertions. They're fragile. If the test finds an assertion that fails, it will stop execution of that method and move on to the next test.
+
+If I had a test with 4 assertions, of which numbers 2, 3, and 4 were all going to fail, you'd only see #2 as the cause of the failed test. If you were to correct that failure, then rerun the test, it would be #3 thats causes grief.
+
+Got it? Good, there will be a pop quiz on Monday.
+
+
+== The Test Case Life Cycle ==
+
+=== 4.1 A Quick Recap ===
+
+You already saw a simple test case in action. It looked something like this:
+
+[source,ruby]
+---------------------------------------------------------
+require 'test/unit'
+
+class MyTestCase < Test::Unit::TestCase
+
+ def test_1
+ assert true
+ end
+
+ def test_2
+ end
+
+ def test_3
+ end
+
+end
+---------------------------------------------------------
+
+You saw that:
+
+ * we need to use `require ‘test/unit'`
+ * we need to inherit from `Test::Unit::TestCase`
+ * we need to define methods that start with `test`
+ * we need assertions to prove our code works
+
+Next, we're going to look at the flow of how test cases are run.
+
+
+=== The Flow ===
+
+When a test case is run, the testing framework creates a ‘fresh' object before running each test. That allows the test to not have to worry about what state the other test methods leave the object in. So for the above example, the testing flow looks like this when run:
+
+ * an object of class `MyTestCase` is created
+ * method `test_1` is run
+ * the test case object is destroyed
+
+then...
+
+ * a brand new object of class `MyTestCase` is created
+ * method `test_2` is run
+ * the test case object is destroyed
+
+then...
+
+ * one last object of class `MyTestCase` is created
+ * method `test_3` is run
+ * the test case object is destroyed
+
+
+=== Setup and Teardown Exposed ===
+
+Now, there are 2 special methods that you can use to hook into this process. One is called setup and the other is called teardown.
+
+Let's rewrite our test.
+
+[source,ruby]
+------------------------------------------
+require 'test/unit'
+
+class MyTestCase < Test::Unit::TestCase
+
+ # called before every single test
+ def setup
+ @name = 'jimmy'
+ @age = 150
+ end
+
+ # called after every single test
+ def teardown
+ end
+
+ # our tests
+ def test_1
+ assert true
+ end
+
+ def test_2
+ end
+
+ def test_3
+ end
+end
+------------------------------------------
+
+The `setup` method will always be called just before the test method. Comparatively, the `teardown` method will always be called always be called just after the test method. So now, the flow looks like this:
+
+ * an object of class `MyTestCase` is created
+ * method `setup` is run
+ * method `test_1` is run
+ * method `teardown` is run
+ * the test case object is destroyed
+
+then...
+
+ * a brand new object of class `MyTestCase` is created
+ * method `setup` is run
+ * method `test_2` is run
+ * method `teardown` is run
+ * the test case object is destroyed
+
+then...
+
+ * one last object of class `MyTestCase` is created
+ * method `setup` is run
+ * method `test_3` is run
+ * method `teardown` is run
+ * the test case object is destroyed
+
+What can you do with this? Well, the `setup` method is good for stuff like creating objects that each method uses. For example, maybe we need to create a user and populate her with sample data before running each of the tests?
+
+As you'll see later, Rails uses the `setup` and `teardown` methods extensively.
+
+
+== Hey Test/Unit. Assert This! ==
+
+=== The Assertion Lineup ===
+
+By now you've caught a glimpse of some of the assertions that are available. Assertions are the worker bees of testing. They are the ones that actually perform the checks to ensure things are going as planned.
+
+There are a bunch of different types of assertions you can use. Here's the complete list of assertions that ship with test/unit. The [msg] is an optional string message you can specify to make your test failure messages clearer. It's not required.
+
+`assert( boolean, [msg] )`::
+ Ensures that the object/expression is true.
+
+`assert_equal( obj1, obj2, [msg] )`::
+ Ensures that `obj1 == obj2` is true.
+
+`assert_not_equal( obj1, obj2, [msg] )`::
+ Ensures that `obj1 == obj2` is false.
+
+`assert_same( obj1, obj2, [msg] )`::
+ Ensures that `obj1.equal?(obj2)` is true.
+
+`assert_not_same( obj1, obj2, [msg] )`::
+ Ensures that `obj1.equal?(obj2)` is false.
+
+`assert_nil( obj, [msg] )`::
+ Ensures that `obj.nil?` is true.
+
+`assert_not_nil( obj, [msg] )`::
+ Ensures that `obj.nil?` is false.
+
+`assert_match( regexp, string, [msg] )`::
+ Ensures that a string matches the regular expression.
+
+`assert_no_match( regexp, string, [msg] )`::
+ Ensures that a string doesn't matches the regular expression.
+
+`assert_in_delta( expecting, actual, delta, [msg] )`::
+ Ensures that the numbers `expecting` and `actual` are within `delta` of each other.
+
+`assert_throws( symbol, [msg] ) { block }`::
+ Ensures that the given block throws the symbol.
+
+`assert_raises( exception1, exception2, ... ) { block }`::
+ Ensures that the given block raises one of the given exceptions.
+
+`assert_nothing_raised( exception1, exception2, ... ) { block }`::
+ Ensures that the given block doesn't raise one of the given exceptions.
+
+`assert_instance_of( class, obj, [msg] )`::
+ Ensures that `obj` is of the `class` type.
+
+`assert_kind_of( class, obj, [msg] )`::
+ Ensures that `obj` is or descends from `class`.
+
+`assert_respond_to( obj, symbol, [msg] )`::
+ Ensures that obj has a method called symbol.
+
+`assert_operator( obj1, operator, obj2, [msg] )`::
+ Ensures that `obj1.operator(obj2)` is true.
+
+`assert_send( array, [msg] )`::
+ Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true. This one is weird eh?
+
+`flunk( [msg] )`::
+ Ensures failure... like me and high school chemistry exams.
+
+Because of the modular nature of the testing framework, it is possible to create your own assertions. In fact, that's exactly what Rails does. It has some specialized assertions to make your life easier.
+
+Creating your own assertions is more of an advanced topic we won't cover in this tutorial.
+
+
+== The Rails Fly-By ==
+
+=== It's About Frikkin' Time ===
+
+Finally we get to testing in with Ruby on Rails! By this point, I'm going to assume a few things.
+
+ * you're familiar with the basic building blocks of 'test/unit'
+ * you know that there are a bunch of assertions you can use
+ * you know what the special methods `setup` and `teardown` are
+ * you're conscious
+
+Rails promotes testing. The Ruby on Rails framework itself was built using these testing methodologies.
+
+In fact, the features built-in to Rails makes it exceptionally easy to do testing. For every model and controller you create using the `script/generate model` and `script/generate controller` scripts, corresponding test stubs are created.
+
+=== Where They Live ===
+
+Your tests, quite surprisingly, go in your 'test' directory under your rails application. In the test directory, you'll see 4 sub-directories; one for controller tests ('functional'), one for model tests ('unit'), one for mock objects ('mocks') and one that only holds sample data ('fixtures').
+
+ * test
+ - /unit
+ - /functional
+ - /fixtures
+ - /mocks
+
+=== How to Turn Them On ===
+
+To run these tests, you simply run the test script directly:
+
+---------------------------------------------
+ruby test/unit/my_good_old_test_unit.rb
+---------------------------------------------
+
+Another way to run your tests is to have the main rakefile run it for you. `rake test_functional` will run all your controller tests and `rake test_units` will run all your model tests.
+
+=== The 3 Environments ===
+
+Testing support was woven into the Rails fabric from the beginning. It wasn't a ``oh! let's bolt on support for running tests because they're new and cool'' epiphany.
+
+Each Rails application you build has 3 sides. A side for production, a side for development and a side for testing.
+
+Let's take a closer look at the Rails 'config/database.yml' file. This YAML configuration file has 3 different sections defining 3 unique database setups:
+
+ * production
+ * development
+ * test
+
+Why 3 different databases? Well, there's one for testing where you can use sample data, there's one for development where you'll be most of the time as you develop your application, and then production for the ``real deal'', or when it goes live.
+
+Every new Rails application should have these 3 sections filled out. They should point to different databases. You may end up not having a local database for your production environment, but development and test should both exist and be different.
+
+=== Why Make This Distinction? ===
+
+If you stop and think about it for a second, it makes sense.
+
+By segregating your development database and your testing database, you're not in any danger of losing any data where it matters.
+
+For example, you need to test your new `delete_this_user_and_every_everything_associated_with_it` function. Wouldn't you want to run this in an environment which makes no difference if you destroy data or not?
+
+When you do end up destroying your testing database (and it will happen, trust me), simply run a task in your rakefile to rebuild it from scratch according to the specs defined in the development database. You can do this by running `rake db:test:prepare`.
+
+
+== The Lo-Down on Fixtures ==
+
+=== What They Are ===
+
+The structure is one thing, but what about when I want to automatically create sample data?
+
+Enter fixtures. Fixtures is a fancy word for ‘sample data'. Fixtures allow you to populate your testing database with predefined data before your tests run. Fixtures are database independent and assume one of two formats: *YAML* or *CSV*.
+
+You'll find fixtures under your 'test/fixtures' directory. When you run `script/generate model` to create a new model, fixture stubs will be automatically created and placed in this directory.
+
+=== YAML the Camel is a Mammal with Enamel ===
+
+YAML-formatted fixtures are a very human-friendly way to describe your sample data. These types of fixtures have the *.yml* file extension (as in 'users.yml').
+
+On any given Sunday, a YAML fixture file may look like this:
+
+---------------------------------------------
+# low & behold! I am a YAML comment!
+david:
+ id: 1
+ name: David Heinemeier Hansson
+ birthday: 1979-10-15
+ profession: Systems development
+
+steve:
+ id: 2
+ name: Steve Ross Kellock
+ birthday: 1974-09-27
+ profession: guy with keyboard
+---------------------------------------------
+
+Each fixture is given a 'name' followed by an indented list of colon-separated key/value pairs. Records are separated by a blank space. You can place comments by using the # character in the first column.
+
+=== Comma Seperated ===
+
+Fixtures can also be described using the all-too-familiar comma-separated value file format. These files, just like YAML fixtures are placed in the 'test/fixtures directory', but these end with the *.csv* file extension (as in 'celebrity_holiday_figures.csv').
+
+A CSV fixture looks like this:
+
+--------------------------------------------------------------
+id, username, password, stretchable, comments
+1, sclaus, ihatekids, false, I like to say ""Ho! Ho! Ho!""
+2, ebunny, ihateeggs, true, Hoppity hop y'all
+3, tfairy, ilovecavities, true, "Pull your teeth, I will"
+--------------------------------------------------------------
+
+The first line is the header. It is a comma-separated list of fields. The rest of the file is the payload: 1 record per line. A few notes about this format:
+
+ * each cell is stripped of outward facing spaces
+ * if you use a comma as data, the cell must be encased in quotes
+ * if you use a quote as data, you must escape it with a 2nd quote
+ * don't use blank lines
+ * nulls can be achived by just placing a comma, for example, (1,sclaus,,false,) minus the parenthesis of course.
+
+Unlike the YAML format where you give each fixture a name, CSV fixture names are automatically generated. They follow a pattern of ``model-name''-''counter''. In the above example, you would have:
+
+--------------------------------------------------------------
+celebrity-holiday-figures-1
+celebrity-holiday-figures-2
+celebrity-holiday-figures-3
+--------------------------------------------------------------
+
+The CSV format is great to use if you have existing data in a spreadsheet or database and you are able to save it (or export it) as a CSV.
+
+=== ERb'in It Up ===
+
+ERb allows you embed ruby code within templates. Both the YAML and CSV fixture formats are pre-processed with ERb. This allows you to use Ruby to help you generate some sample data.
+
+I'll demonstrate with a YAML file:
+
+--------------------------------------------------------------
+<% earth_size = 20 -%>
+mercury:
+ id: 1
+ size: <%= earth_size / 50 %>
+
+venus:
+ id: 2
+ size: <%= earth_size / 2 %>
+
+mars:
+ id: 3
+ size: <%= earth_size - 69 %>
+--------------------------------------------------------------
+
+Anything encased within the
+
+-------------
+<% -%>
+-------------
+
+tag is considered Ruby code. To actually print something out, you must use the
+
+-------------
+<%= %>
+-------------
+
+tag.
+
+=== Fixtures in Action ===
+
+Rails makes no assumptions when it comes to fixtures. You must explicitly load them yourself by using the fixtures method within your TestCase. For example, a users model unit test might look like this:
+
+[source, ruby]
+--------------------------------------------------------------
+# Allow this test to hook into the Rails framework.
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserTest < Test::Unit::TestCase
+ fixtures :users
+
+ # Count the fixtures.
+ def test_count_my_fixtures
+ assert_equal 5, User.count
+ end
+end
+--------------------------------------------------------------
+
+Using the fixtures method and placing the symbol name of the model, Rails will automatically load up the fixtures for you at the start of each test method.
+
+[source, ruby]
+--------------------------------------------------------------
+fixtures :users
+--------------------------------------------------------------
+
+What exactly does this line of code do? It does 3 things:
+
+ * it nukes any existing data living in the users table
+* it loads the fixture data (if any) into the users table
+ * it dumps the data into a variable in case you want to access it directly
+
+So, in the above example, if we had another test method, we wouldn't have 10 users on the 2nd test because they would be wiped out before being created.
+
+You can load multiple fixtures by including them on the same line separated by commas.
+
+[source, ruby]
+--------------------------------------------------------------
+fixtures :users, :losers, :bruisers, :cruisers
+--------------------------------------------------------------
+
+=== Hashes with Special Powers ===
+
+Fixtures are basically Hash objects. As mentioned in point #3 above, you can access the hash object directly because it is automatically setup as a local variable of the test case.
+
+[source, ruby]
+--------------------------------------------------------------
+...
+ fixtures :users
+
+ def test_user
+ # this will return the Hash for the fixture named david
+ users(:david)
+
+ # this will return the property for david called id
+ users(:david).id
+ end
+...
+--------------------------------------------------------------
+
+But, by there's another side to fixtures... at night, if the moon is full and the wind completely still, fixtures can also transform themselves into the form of the original class!
+
+Now you can get at the methods only available to that class.
+
+[source, ruby]
+--------------------------------------------------------------
+...
+ fixtures :users
+
+ def test_user
+ # using the find method, we grab the "real" david as a User
+ david = users(:david).find
+
+ # an now we have access to methods only available to a User class
+ email( david.girlfriend.email, david.illegitimate_children )
+ end
+...
+--------------------------------------------------------------
+
+
+== Testing Your Models ==
+
+=== A Unit Test ===
+
+Unit tests are what we use to test our models. Generally, there is one test for each model. The test stubs are created automatically when you use `script/generate model SecretAgent` (for example).
+
+Let's take a look at what a unit test might look like.
+
+[source, ruby]
+-------------------------------------------------------
+# hook into the Rails environment
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SecretAgent < ActiveSupport::TestCase
+ fixtures :secret_agents
+
+ # ensure the SecretAgent plays well with the database
+ def test_create_read_update_delete
+ # create a brand new secret agent
+ jimmy = SecretAgent.new("jagent", "unbelievablysecretpassword")
+
+ # save him
+ assert jimmy.save
+
+ # read him back
+ agent = SecretAgent.find(jimmy.id)
+
+ # compare the usernames
+ assert_equal jimmy.username, agent.username
+
+ # change the password by using hi-tech encryption
+ agent.username = agent.username.reverse
+
+ # save the changes
+ assert agent.save
+
+ # the agent gets killed
+ assert agent.destroy
+ end
+end
+-------------------------------------------------------
+
+In this basic unit test, we're exercising the 'SecretAgent' model. In our solitary test, we're proving a bunch of things about our 'SecretAgent' model. We try 4 different assertions to test that we can do the basics with the database such as create, read, update and delete (creatively known as CRUD).
+
+It is up to you to decide just how much you want to test of your model. Ideally you test anything that could possibly break, however, it is really trial and error. Only you know what's best to test.
+
+You most certainly want to test the validation code, and additionally, you probably want a least 1 test for every method in your model.
+
+
+== Testing Your Controllers ==
+
+=== What Is It? ===
+
+The goal of functional testing is to test your controllers. When you get into the realm of testing controllers, we're operating at a higher level than the model. At this level, we test for things such as:
+
+ * was the web request successful?
+ * were we redirected to the right page?
+ * were we successfully authenticated?
+ * was the correct object stored in the response template?
+
+Just as there is a one-to-one ratio between unit tests and models, so there is between functional tests and controllers. For a controller named `HomeController`, you would have a test case named `HomeControllerTest`.
+
+=== An Anatomy Lesson ===
+
+So let's take a look at an example of a functional test.
+
+[source, ruby]
+----------------------------------------------------
+require File.dirname(__FILE__) + '/../test_helper'
+
+class HomeControllerTest < ActionController::TestCase
+ # let's test our main index page
+ def test_index
+ get :index
+ assert_response :success
+ end
+end
+----------------------------------------------------
+
+==== Making the moves ====
+
+In the one test we have called `test_index`, we are simulating a request on the action called index and making sure the request was successful.
+
+The `get` method kicks off the web request and populates the results into the response. It accepts 4 arguments.
+
+ * The action of the controller you are requesting. It can be in the form of a string or a symbol. Cool people use symbols. ;)
+ * An optional hash of request parameters to pass into the action (eg. query string parameters or post variables).
+ * An optional hash of session variables to pass along with the request.
+ * An optional hash of flash to stash your goulash.
+
+*Example:* Calling the `:show` action, passing an `id` of 12 as the params and setting `user_id` of 5 in the session.
+
+[source, ruby]
+----------------------------------------------------
+get(:show, {'id' => "12"}, {'user_id' => 5})
+----------------------------------------------------
+
+*Another example:* Calling the `:view` action, passing an `id` of 12 as the params, this time with no session, but with a flash message.
+
+[source, ruby]
+----------------------------------------------------
+get(:view, {'id' => '12'}, nil, {'message' => 'booya!'})
+----------------------------------------------------
+
+==== Available at your disposal ====
+
+For those of you familiar with HTTP protocol, you'll know that get is a type of request. There are 5 request types supported in Rails:
+
+ * get
+ * post
+ * put
+ * head
+ * delete
+
+All of request types are methods that you can use, however, you'll probably end up using the first two more ofter than the others.
+
+=== The 4 Hashes of the Apocolypse ===
+
+After the request has been made by using one of the 5 methods (get, post, etc...), you will have 4 Hash objects ready for use.
+
+They are (starring in alphabetical order):
+
+`assigns`::
+ Any objects that are stored as instance variables in actions for use in views.
+`cookies`::
+ Any objects cookies that are set.
+`flash`::
+ Any objects living in the flash.
+`session`::
+ Any object living in session variables.
+
+For example, let's say we have a `MoviesController` with an action called `movie`. The code for that action might look something like:
+
+[source, ruby]
+----------------------------------------------------
+def movie
+ @movie = Movie.find(params[:id])
+ if @movie.nil?
+ flash['message'] = "That movie has been burned."
+ redirect_to :controller => 'error', :action => 'missing'
+ end
+end
+----------------------------------------------------
+
+Now, to test out if the proper movie is being set, we could have a series of tests that look like this:
+
+[source, ruby]
+----------------------------------------------------
+# this test proves that fetching a movie works
+def test_successfully_finding_a_movie
+ get :movie, "id" => "1"
+ assert_not_nil assigns["movie"]
+ assert_equal 1, assigns["movie"].id
+ assert flash.empty?
+end
+
+# and when we can't find a movie...
+def test_movie_not_found
+ get :movie, "id" => "666999"
+ assert_nil assigns["movie"]
+ assert flash.has_key?("message")
+ assert assigns.empty?
+end
+----------------------------------------------------
+
+As is the case with normal Hash objects, you can access the values by referencing the keys by string. You can also reference them by symbol name... except assigns. Check it out:
+
+[source, ruby]
+----------------------------------------------------
+flash["gordon"] flash[:gordon]
+session["shmession"] session[:shmession]
+cookies["are_good_for_u"] cookies[:are_good_for_u]
+
+# Because you can't use assigns[:something] for historical reasons:
+assigns["something"] assigns(:something)
+----------------------------------------------------
+
+Keep an eye out for that. mmmm kay?
+
+=== Response-Related Assertions ===
+
+There are 3 assertions that deal with the overall response to a request. They are:
+
+`assert_template( expected_template, [msg] )`::
+Ensures the expected template was responsible for rendering. For example:
++
+[source, ruby]
+--------------------------------------
+assert_template "user/profile"
+--------------------------------------
++
+This code will fail unless the template located at app/views/user/profile.rhtml was rendered.
+
+`assert_response( type_or_code, [msg] )`::
+Ensures the response type/status code is as expected. For example:
++
+[source, ruby]
+--------------------------------------
+assert_response :success # page rendered ok
+assert_response :redirect # we've been redirected
+assert_response :missing # not found
+assert_response 505 # status code was 505
+--------------------------------------
++
+The possible options are:
++
+ * `:success` (status code is 200)
+ * `:redirect` (status code is within 300..399)
+ * `:missing` (status code is 404)
+ * `:error` (status code is within 500..599)
+ * any number (to specifically reference a particular status code)
+
+`assert_redirected_to ( options={}, [msg] )`::
+Ensures we've been redirected to a specific place within our application.
++
+[source, ruby]
+--------------------------
+assert_redirected_to :controller => 'widget', :action => 'view', :id => 555
+--------------------------
+
+=== Tag-Related Assertions ===
+
+The assert_tag and assert_no_tag assertions are for analysing the html returned from a request.
+
+==== assert_tag( options ) ====
+Ensures that a tag or text exists. There are a whole whack o' options you can use to discover what you are looking for. Some of the conditions are like XPATH in concept, but this is sexier. In fact, let's call it SEXPATH.
+
+The following description is lifted verbatim from the rails assertion docs.
+
+Asserts that there is a tag/node/element in the body of the response that meets all of the given conditions. The conditions parameter must be a hash of any of the following keys (all are optional):
+
+ * `:tag` : the node type must match the corresponding value
+ * `:attributes` : a hash. The node's attributes must match the corresponding values in the hash.
+ * `:parent` : a hash. The node's parent must match the corresponding hash.
+ * `:child` : a hash. At least one of the node's immediate children must meet the criteria described by the hash.
+ * `:ancestor` : a hash. At least one of the node's ancestors must meet the criteria described by the hash.
+ * `:descendant` : a hash. At least one of the node's descendants must meet the criteria described by the hash.
+ * `:children` : a hash, for counting children of a node. Accepts the keys:
+ - `:count` : either a number or a range which must equal (or include) the number of children that match.
+ - `:less_than` : the number of matching children must be less than this number.
+ - `:greater_than` : the number of matching children must be greater than this number.
+ - `:only` : another hash consisting of the keys to use to match on the children, and only matching children will be counted.
+ - `:content` : (text nodes only). The content of the node must match the given value.
+
+Conditions are matched using the following algorithm:
+
+ * if the condition is a *string*, it must be a substring of the value.
+ * if the condition is a *regexp*, it must match the value.
+ * if the condition is a *number*, the value must match `number.to_s`.
+ * if the condition is *true*, the value must not be `nil`.
+ * if the condition is *false* or *nil*, the value must be `nil`.
+
+These examples are taken from the same docs too:
+
+[source, ruby]
+-------------------------------------------------------------
+ # assert that there is a "span" tag
+ assert_tag :tag => "span"
+
+ # assert that there is a "span" inside of a "div"
+ assert_tag :tag => "span", :parent => { :tag => "div" }
+
+ # assert that there is a "span" somewhere inside a table
+ assert_tag :tag => "span", :ancestor => { :tag => "table" }
+
+ # assert that there is a "span" with at least one "em" child
+ assert_tag :tag => "span", :child => { :tag => "em" }
+
+ # assert that there is a "span" containing a (possibly nested)
+ # "strong" tag.
+ assert_tag :tag => "span", :descendant => { :tag => "strong" }
+
+ # assert that there is a "span" containing between 2 and 4 "em" tags
+ # as immediate children
+ assert_tag :tag => "span",
+ :children => { :count => 2..4, :only => { :tag => "em" } }
+
+ # get funky: assert that there is a "div", with an "ul" ancestor
+ # and an "li" parent (with "class" = "enum"), and containing a
+ # "span" descendant that contains text matching /hello world/
+ assert_tag :tag => "div",
+ :ancestor => { :tag => "ul" },
+ :parent => { :tag => "li",
+ :attributes => { :class => "enum" } },
+ :descendant => { :tag => "span",
+ :child => /hello world/ }
+-------------------------------------------------------------
+
+==== assert_no_tag( options ) ====
+This is the exact opposite of assert_tag. It ensures that the tag does not exist.
+
+=== Routing-Related Assertions ===
+
+==== assert_generates( expected_path, options, defaults={}, extras = {}, [msg] ) ====
+Ensures that the options map to the expected_path.
+
+[source, ruby]
+-------------------------------------------------------------
+opts = {:controller => "movies", :action => "movie", :id => "69"}
+assert_generates "movies/movie/69", opts
+-------------------------------------------------------------
+
+==== assert_recognizes( expected_options, path, extras={}, [msg] ) ====
+Ensures that when the path is chopped up into pieces, it is equal to expected_options. Essentially, the opposite of assert_generates.
+
+[source, ruby]
+-------------------------------------------------------------
+opts = {:controller => "movies", :action => "movie", :id => "69"}
+assert_recognizes opts, "/movies/movie/69
+
+# also, let's say i had a line in my config/routes.rb
+# that looked like:
+#
+# map.connect (
+# 'calendar/:year/:month',
+# :controller => 'content',
+# :action => 'calendar',
+# :year => nil,
+# :month => nil,
+# :requirements => {:year => /\d{4}/, :month => /\d{1,2}/}
+# }
+#
+# Then, this would work too:
+opts = {
+ :controller => 'content',
+ :action => 'calendar',
+ :year => '2005',
+ :month => '5'
+}
+assert_recognizes opts, 'calendar/2005/5'
+-------------------------------------------------------------
+
+==== assert_routing( path, options, defaults={}, extras={}, [msg] ) ====
+Ensures that the path resolves into options, and the options, resolves into path. It's a two-way check to make sure your routing maps work as expected.
+
+This assertion is simply a wrapper around `assert_generates` and `assert_recognizes`.
+
+If you're going to test your routes, this assertion might be your best bet for robustness (yes, the overused buzzword of the 90's).
+
+[source, ruby]
+-------------------------------------------------------------
+opts = {:controller => "movies", :action => "movie", :id => "69"}
+assert_routing "movies/movie/69", opts
+-------------------------------------------------------------
+
+=== Testing File Uploads ===
+So your web app supports file uploads eh? Here's what you can do to test your uploads.
+
+This tip is brought to you by Chris Brinker, the letter R and the number 12.
+
+Chris says, '``In order to test a file being uploaded you have to mirror what cgi.rb is doing with a multipart post. Unfortunately what it does is quite long and complex, this code takes a file on your system, and turns it into what normally comes out of cgi.rb.'''
+
+Here are some helper methods based on Chris' work that you'll need to squirrel away either in a new unit, or cut ‘n' pasted right into your test. Any errors with this are my fault.
+
+[source, ruby]
+-------------------------------------------------------------
+# get us an object that represents an uploaded file
+def uploaded_file(path, content_type="application/octet-stream", filename=nil)
+ filename ||= File.basename(path)
+ t = Tempfile.new(filename)
+ FileUtils.copy_file(path, t.path)
+ (class << t; self; end;).class_eval do
+ alias local_path path
+ define_method(:original_filename) { filename }
+ define_method(:content_type) { content_type }
+ end
+ return t
+end
+
+# a JPEG helper
+def uploaded_jpeg(path, filename=nil)
+ uploaded_file(path, 'image/jpeg', filename)
+end
+
+# a GIF helper
+def uploaded_gif(path, filename=nil)
+ uploaded_file(path, 'image/gif', filename)
+end
+-------------------------------------------------------------
+
+And to use this code, you'd have a test that would looks something like this:
+
+[source, ruby]
+-------------------------------------------------------------
+def test_a_file_upload
+ assert_equal 0, GalleryImage.count
+ heman = uploaded_jpeg("#{File.expand_path(RAILS_ROOT)}/text/fixtures/heman.jpg")
+ post :imageupload, 'imagefile' => heman
+ assert_redirected_to :controller => 'gallery', :action => 'view'
+ assert_equal 1, GalleryImage.count
+end
+-------------------------------------------------------------
+
+
+== Testing Your Mailers ==
+
+=== Keeping the postman in check ===
+
+Your ActionMailer -- like every other part of your Rails application -- should be tested to ensure that it is working as expected.
+
+The goal of testing your ActionMailer is to ensure that:
+
+ * emails are being processed (created and sent)
+ * the email content is correct (subject, sender, body, etc)
+ * the right emails are being sent at the righ times
+
+==== From all sides ====
+
+There are two aspects of testing your mailer, the unit tests and the functional tests.
+Unit tests
+
+In the unit tests, we run the mailer in isolation with tightly controlled inputs and compare the output to a known-value -- a fixture -- yay! more fixtures!
+
+==== Functional tests ====
+
+In the functional tests we don't so much test the minute details produced by the mailer, instead we test that our controllers and models are using the mailer in the right way. We test to prove that the right email was sent at the right time.
+
+=== Unit Testing ===
+
+In order to test that your mailer is working as expected, we can use unit tests to compare the actual results of the mailer with pre-writen examples of what should be produced.
+
+==== Revenge of the fixtures ====
+
+For the purposes of unit testing a mailer, fixtures are used to provide an example of how output ``should'' look. Because these are example emails, and not Active Record data like the other fixtures, they are kept in their own subdirectory from the other fixtures. Don't tease them about it though, they hate that.
+
+When you generated your mailer (you did that right?) the generator created stub fixtures for each of the mailers actions. If you didn't use the generator you'll have to make those files yourself.
+
+==== The basic test case ====
+
+Here is an example of what you start with.
+
+[source, ruby]
+-------------------------------------------------
+require File.dirname(__FILE__) + '/. ./test_helper'
+
+class MyMailerTest < Test::Unit::TestCase
+ FIXTURES_PATH = File.dirname(__FILE__) + '/. ./fixtures'
+
+ def setup
+ ActionMailer::Base.delivery_method = :test
+ ActionMailer::Base.perform_deliveries = true
+ ActionMailer::Base.deliveries = []
+
+ @expected = TMail::Mail.new
+ end
+
+ def test_signup
+ @expected.subject = 'MyMailer#signup'
+ @expected.body = read_fixture('signup')
+ @expected.date = Time.now
+
+ assert_equal @expected.encoded, MyMailer.create_signup(@expected.date).encoded
+ end
+
+ private
+ def read_fixture(action)
+ IO.readlines("#{FIXTURES_PATH}/my_mailer/#{action}")
+ end
+end
+-------------------------------------------------
+
+The `setup` method is mostly concerned with setting up a blank slate for the next test. However it is worth describing what each statement does
+
+[source, ruby]
+-------------------------------------------------
+ActionMailer::Base.delivery_method = :test
+-------------------------------------------------
+
+sets the delivery method to test mode so that email will not actually be delivered (useful to avoid spamming your users while testing) but instead it will be appended to an array (ActionMailer::Base.deliveries).
+
+[source, ruby]
+-------------------------------------------------
+ActionMailer::Base.perform_deliveries = true
+-------------------------------------------------
+
+Ensures the mail will be sent using the method specified by ActionMailer::Base.delivery_method, and finally
+
+[source, ruby]
+-------------------------------------------------
+ActionMailer::Base.deliveries = []
+-------------------------------------------------
+
+sets the array of sent messages to an empty array so we can be sure that anything we find there was sent as part of our current test.
+
+However often in unit tests, mails will not actually be sent, simply constructed, as in the example above, where the precise content of the email is checked against what it should be. Dave Thomas suggests an alternative approach, which is just to check the part of the email that is likely to break, i.e. the dynamic content. The following example assumes we have some kind of user table, and we might want to mail those users new passwords:
+
+[source, ruby]
+-------------------------------------------------
+require File.dirname(__FILE__) + '/../test_helper'
+require 'my_mailer'
+
+class MyMailerTest < Test::Unit::TestCase
+ fixtures :users
+ FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
+ CHARSET = "utf-8"
+
+ include ActionMailer::Quoting
+
+ def setup
+ ActionMailer::Base.delivery_method = :test
+ ActionMailer::Base.perform_deliveries = true
+ ActionMailer::Base.deliveries = []
+
+ @expected = TMail::Mail.new
+ @expected.set_content_type "text", "plain", { "charset" => CHARSET }
+ end
+
+ def test_reset_password
+ user = User.find(:first)
+ newpass = 'newpass'
+ response = MyMailer.create_reset_password(user,newpass)
+ assert_equal 'Your New Password', response.subject
+ assert_match /Dear #{user.full_name},/, response.body
+ assert_match /New Password: #{newpass}/, response.body
+ assert_equal user.email, response.to[0]
+ end
+
+ private
+ def read_fixture(action)
+ IO.readlines("#{FIXTURES_PATH}/community_mailer/#{action}")
+ end
+
+ def encode(subject)
+ quoted_printable(subject, CHARSET)
+ end
+end
+-------------------------------------------------
+
+and here we check the dynamic parts of the mail, specifically that we use the users' correct full name and that we give them the correct password.
+
+=== Functional Testing ===
+
+Functional testing involves more than just checking that the email body, recipients and so forth are correct. In functional mail tests we call the mail deliver methods and check that the appropriate emails have been appended to the delivery list. It is fairly safe to assume that the deliver methods themselves do their job -- what we are probably more interested in is whether our own business logic is sending emails when we expect them to. For example the password reset operation we used an example in the previous section will probably be called in response to a user requesting a password reset through some sort of controller.
+
+[source, ruby]
+----------------------------------------------------------------
+require File.dirname(__FILE__) + '/../test_helper'
+require 'my_controller'
+
+# Raise errors beyond the default web-based presentation
+class MyController; def rescue_action(e) raise e end; end
+
+class MyControllerTest < Test::Unit::TestCase
+
+ def setup
+ @controller = MyController.new
+ @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
+ end
+
+ def test_reset_password
+ num_deliveries = ActionMailer::Base.deliveries.size
+ post :reset_password, :email => 'bob@test.com'
+
+ assert_equal num_deliveries+1, ActionMailer::Base.deliveries.size
+ end
+
+end
+----------------------------------------------------------------
+
+=== Filtering emails in development ===
+
+Sometimes you want to be somewhere inbetween the `:test` and `:smtp` settings. Say you're working on your development site, and you have a few testers working with you. The site isn't in production yet, but you'd like the testers to be able to receive emails from the site, but no one else. Here's a handy way to handle that situation, add this to your 'environment.rb' or 'development.rb' file
+
+[source, ruby]
+----------------------------------------------------------------
+class ActionMailer::Base
+
+ def perform_delivery_fixed_email(mail)
+ destinations = mail.destinations
+ if destinations.nil?
+ destinations = ["mymail@me.com"]
+ mail.subject = '[TEST-FAILURE]:'+mail.subject
+ else
+ mail.subject = '[TEST]:'+mail.subject
+ end
+ approved = ["testerone@me.com","testertwo@me.com"]
+ destinations = destinations.collect{|x| approved.collect{|y| (x==y ? x : nil)}}.flatten.compact
+ mail.to = destinations
+ if destinations.size > 0
+ mail.ready_to_send
+ Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain],
+ server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp|
+ smtp.sendmail(mail.encoded, mail.from, destinations)
+ end
+ end
+
+ end
+
+end
+----------------------------------------------------------------
+
diff --git a/railties/environments/boot.rb b/railties/environments/boot.rb
index cd21fb9eab..6a30b54973 100644
--- a/railties/environments/boot.rb
+++ b/railties/environments/boot.rb
@@ -82,14 +82,14 @@ module Rails
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/railties/environments/production.rb b/railties/environments/production.rb
index 69c8b9ecb6..ec5b7bc865 100644
--- a/railties/environments/production.rb
+++ b/railties/environments/production.rb
@@ -4,13 +4,15 @@
# Code is not reloaded between requests
config.cache_classes = true
+# Enable threaded mode
+# config.threadsafe!
+
# Use a different logger for distributed setups
# config.logger = SyslogLogger.new
# Full error reports are disabled and caching is turned on
config.action_controller.consider_all_requests_local = false
config.action_controller.perform_caching = true
-config.action_view.cache_template_loading = true
# Use a different cache store in production
# config.cache_store = :mem_cache_store
diff --git a/railties/helpers/performance_test_helper.rb b/railties/helpers/performance_test_helper.rb
index f1662fa30a..1aafc7f7e5 100644
--- a/railties/helpers/performance_test_helper.rb
+++ b/railties/helpers/performance_test_helper.rb
@@ -1,7 +1,2 @@
require 'test_helper'
-require 'action_controller/performance_test'
-
-ActionController::Base.perform_caching = true
-ActionView::Base.cache_template_loading = true
-ActiveSupport::Dependencies.mechanism = :require
-Rails.logger.level = ActiveSupport::BufferedLogger::INFO
+require 'performance_test_help'
diff --git a/railties/html/500.html b/railties/html/500.html
index 0e9c14f4c6..74142cb04a 100644
--- a/railties/html/500.html
+++ b/railties/html/500.html
@@ -25,6 +25,9 @@
<div class="dialog">
<h1>We're sorry, but something went wrong.</h1>
<p>We've been notified about this issue and we'll take a look at it shortly.</p>
+ <p><small>(If you're the administrator of this website, then please read
+ the log file "<%= "<%s>" % "%=h RAILS_ENV %" %>.log"
+ to find out what went wrong.)</small></p>
</div>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/railties/html/javascripts/prototype.js b/railties/html/javascripts/prototype.js
index 546f9fe449..2c70b8a7e8 100644
--- a/railties/html/javascripts/prototype.js
+++ b/railties/html/javascripts/prototype.js
@@ -1,5 +1,5 @@
-/* Prototype JavaScript framework, version 1.6.0.1
- * (c) 2005-2007 Sam Stephenson
+/* Prototype JavaScript framework, version 1.6.0.2
+ * (c) 2005-2008 Sam Stephenson
*
* Prototype is freely distributable under the terms of an MIT-style license.
* For details, see the Prototype web site: http://www.prototypejs.org/
@@ -7,7 +7,7 @@
*--------------------------------------------------------------------------*/
var Prototype = {
- Version: '1.6.0.1',
+ Version: '1.6.0.2',
Browser: {
IE: !!(window.attachEvent && !window.opera),
@@ -110,7 +110,7 @@ Object.extend(Object, {
try {
if (Object.isUndefined(object)) return 'undefined';
if (object === null) return 'null';
- return object.inspect ? object.inspect() : object.toString();
+ return object.inspect ? object.inspect() : String(object);
} catch (e) {
if (e instanceof RangeError) return '...';
throw e;
@@ -171,7 +171,8 @@ Object.extend(Object, {
},
isArray: function(object) {
- return object && object.constructor === Array;
+ return object != null && typeof object == "object" &&
+ 'splice' in object && 'join' in object;
},
isHash: function(object) {
@@ -578,7 +579,7 @@ var Template = Class.create({
}
return before + String.interpret(ctx);
- }.bind(this));
+ });
}
});
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
@@ -806,20 +807,20 @@ Object.extend(Enumerable, {
function $A(iterable) {
if (!iterable) return [];
if (iterable.toArray) return iterable.toArray();
- var length = iterable.length, results = new Array(length);
+ var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length];
return results;
}
if (Prototype.Browser.WebKit) {
- function $A(iterable) {
+ $A = function(iterable) {
if (!iterable) return [];
if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
iterable.toArray) return iterable.toArray();
- var length = iterable.length, results = new Array(length);
+ var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length];
return results;
- }
+ };
}
Array.from = $A;
@@ -1298,7 +1299,7 @@ Ajax.Request = Class.create(Ajax.Base, {
var contentType = response.getHeader('Content-type');
if (this.options.evalJS == 'force'
- || (this.options.evalJS && contentType
+ || (this.options.evalJS && this.isSameOrigin() && contentType
&& contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
this.evalResponse();
}
@@ -1316,9 +1317,18 @@ Ajax.Request = Class.create(Ajax.Base, {
}
},
+ isSameOrigin: function() {
+ var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
+ return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
+ protocol: location.protocol,
+ domain: document.domain,
+ port: location.port ? ':' + location.port : ''
+ }));
+ },
+
getHeader: function(name) {
try {
- return this.transport.getResponseHeader(name);
+ return this.transport.getResponseHeader(name) || null;
} catch (e) { return null }
},
@@ -1391,7 +1401,8 @@ Ajax.Response = Class.create({
if (!json) return null;
json = decodeURIComponent(escape(json));
try {
- return json.evalJSON(this.request.options.sanitizeJSON);
+ return json.evalJSON(this.request.options.sanitizeJSON ||
+ !this.request.isSameOrigin());
} catch (e) {
this.request.dispatchException(e);
}
@@ -1404,7 +1415,8 @@ Ajax.Response = Class.create({
this.responseText.blank())
return null;
try {
- return this.responseText.evalJSON(options.sanitizeJSON);
+ return this.responseText.evalJSON(options.sanitizeJSON ||
+ !this.request.isSameOrigin());
} catch (e) {
this.request.dispatchException(e);
}
@@ -1608,24 +1620,28 @@ Element.Methods = {
Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
insertions = {bottom:insertions};
- var content, t, range;
+ var content, insert, tagName, childNodes;
- for (position in insertions) {
+ for (var position in insertions) {
content = insertions[position];
position = position.toLowerCase();
- t = Element._insertionTranslations[position];
+ insert = Element._insertionTranslations[position];
if (content && content.toElement) content = content.toElement();
if (Object.isElement(content)) {
- t.insert(element, content);
+ insert(element, content);
continue;
}
content = Object.toHTML(content);
- range = element.ownerDocument.createRange();
- t.initializeRange(element, range);
- t.insert(element, range.createContextualFragment(content.stripScripts()));
+ tagName = ((position == 'before' || position == 'after')
+ ? element.parentNode : element).tagName.toUpperCase();
+
+ childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+
+ if (position == 'top' || position == 'after') childNodes.reverse();
+ childNodes.each(insert.curry(element));
content.evalScripts.bind(content).defer();
}
@@ -1670,7 +1686,7 @@ Element.Methods = {
},
descendants: function(element) {
- return $(element).getElementsBySelector("*");
+ return $(element).select("*");
},
firstDescendant: function(element) {
@@ -1709,32 +1725,31 @@ Element.Methods = {
element = $(element);
if (arguments.length == 1) return $(element.parentNode);
var ancestors = element.ancestors();
- return expression ? Selector.findElement(ancestors, expression, index) :
- ancestors[index || 0];
+ return Object.isNumber(expression) ? ancestors[expression] :
+ Selector.findElement(ancestors, expression, index);
},
down: function(element, expression, index) {
element = $(element);
if (arguments.length == 1) return element.firstDescendant();
- var descendants = element.descendants();
- return expression ? Selector.findElement(descendants, expression, index) :
- descendants[index || 0];
+ return Object.isNumber(expression) ? element.descendants()[expression] :
+ element.select(expression)[index || 0];
},
previous: function(element, expression, index) {
element = $(element);
if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
var previousSiblings = element.previousSiblings();
- return expression ? Selector.findElement(previousSiblings, expression, index) :
- previousSiblings[index || 0];
+ return Object.isNumber(expression) ? previousSiblings[expression] :
+ Selector.findElement(previousSiblings, expression, index);
},
next: function(element, expression, index) {
element = $(element);
if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
var nextSiblings = element.nextSiblings();
- return expression ? Selector.findElement(nextSiblings, expression, index) :
- nextSiblings[index || 0];
+ return Object.isNumber(expression) ? nextSiblings[expression] :
+ Selector.findElement(nextSiblings, expression, index);
},
select: function() {
@@ -1860,7 +1875,8 @@ Element.Methods = {
do { ancestor = ancestor.parentNode; }
while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
}
- if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex);
+ if (nextAncestor && nextAncestor.sourceIndex)
+ return (e > a && e < nextAncestor.sourceIndex);
}
while (element = element.parentNode)
@@ -2004,7 +2020,7 @@ Element.Methods = {
if (element) {
if (element.tagName == 'BODY') break;
var p = Element.getStyle(element, 'position');
- if (p == 'relative' || p == 'absolute') break;
+ if (p !== 'static') break;
}
} while (element);
return Element._returnOffset(valueL, valueT);
@@ -2153,46 +2169,6 @@ Element._attributeTranslations = {
}
};
-
-if (!document.createRange || Prototype.Browser.Opera) {
- Element.Methods.insert = function(element, insertions) {
- element = $(element);
-
- if (Object.isString(insertions) || Object.isNumber(insertions) ||
- Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
- insertions = { bottom: insertions };
-
- var t = Element._insertionTranslations, content, position, pos, tagName;
-
- for (position in insertions) {
- content = insertions[position];
- position = position.toLowerCase();
- pos = t[position];
-
- if (content && content.toElement) content = content.toElement();
- if (Object.isElement(content)) {
- pos.insert(element, content);
- continue;
- }
-
- content = Object.toHTML(content);
- tagName = ((position == 'before' || position == 'after')
- ? element.parentNode : element).tagName.toUpperCase();
-
- if (t.tags[tagName]) {
- var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
- if (position == 'top' || position == 'after') fragments.reverse();
- fragments.each(pos.insert.curry(element));
- }
- else element.insertAdjacentHTML(pos.adjacency, content.stripScripts());
-
- content.evalScripts.bind(content).defer();
- }
-
- return element;
- };
-}
-
if (Prototype.Browser.Opera) {
Element.Methods.getStyle = Element.Methods.getStyle.wrap(
function(proceed, element, style) {
@@ -2237,12 +2213,31 @@ if (Prototype.Browser.Opera) {
}
else if (Prototype.Browser.IE) {
- $w('positionedOffset getOffsetParent viewportOffset').each(function(method) {
+ // IE doesn't report offsets correctly for static elements, so we change them
+ // to "relative" to get the values, then change them back.
+ Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
+ function(proceed, element) {
+ element = $(element);
+ var position = element.getStyle('position');
+ if (position !== 'static') return proceed(element);
+ element.setStyle({ position: 'relative' });
+ var value = proceed(element);
+ element.setStyle({ position: position });
+ return value;
+ }
+ );
+
+ $w('positionedOffset viewportOffset').each(function(method) {
Element.Methods[method] = Element.Methods[method].wrap(
function(proceed, element) {
element = $(element);
var position = element.getStyle('position');
- if (position != 'static') return proceed(element);
+ if (position !== 'static') return proceed(element);
+ // Trigger hasLayout on the offset parent so that IE6 reports
+ // accurate offsetTop and offsetLeft values for position: fixed.
+ var offsetParent = element.getOffsetParent();
+ if (offsetParent && offsetParent.getStyle('position') === 'fixed')
+ offsetParent.setStyle({ zoom: 1 });
element.setStyle({ position: 'relative' });
var value = proceed(element);
element.setStyle({ position: position });
@@ -2324,7 +2319,10 @@ else if (Prototype.Browser.IE) {
};
Element._attributeTranslations.write = {
- names: Object.clone(Element._attributeTranslations.read.names),
+ names: Object.extend({
+ cellpadding: 'cellPadding',
+ cellspacing: 'cellSpacing'
+ }, Element._attributeTranslations.read.names),
values: {
checked: function(element, value) {
element.checked = !!value;
@@ -2444,7 +2442,7 @@ if (Prototype.Browser.IE || Prototype.Browser.Opera) {
};
}
-if (document.createElement('div').outerHTML) {
+if ('outerHTML' in document.createElement('div')) {
Element.Methods.replace = function(element, content) {
element = $(element);
@@ -2482,45 +2480,25 @@ Element._returnOffset = function(l, t) {
Element._getContentFromAnonymousElement = function(tagName, html) {
var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
- div.innerHTML = t[0] + html + t[1];
- t[2].times(function() { div = div.firstChild });
+ if (t) {
+ div.innerHTML = t[0] + html + t[1];
+ t[2].times(function() { div = div.firstChild });
+ } else div.innerHTML = html;
return $A(div.childNodes);
};
Element._insertionTranslations = {
- before: {
- adjacency: 'beforeBegin',
- insert: function(element, node) {
- element.parentNode.insertBefore(node, element);
- },
- initializeRange: function(element, range) {
- range.setStartBefore(element);
- }
+ before: function(element, node) {
+ element.parentNode.insertBefore(node, element);
},
- top: {
- adjacency: 'afterBegin',
- insert: function(element, node) {
- element.insertBefore(node, element.firstChild);
- },
- initializeRange: function(element, range) {
- range.selectNodeContents(element);
- range.collapse(true);
- }
+ top: function(element, node) {
+ element.insertBefore(node, element.firstChild);
},
- bottom: {
- adjacency: 'beforeEnd',
- insert: function(element, node) {
- element.appendChild(node);
- }
+ bottom: function(element, node) {
+ element.appendChild(node);
},
- after: {
- adjacency: 'afterEnd',
- insert: function(element, node) {
- element.parentNode.insertBefore(node, element.nextSibling);
- },
- initializeRange: function(element, range) {
- range.setStartAfter(element);
- }
+ after: function(element, node) {
+ element.parentNode.insertBefore(node, element.nextSibling);
},
tags: {
TABLE: ['<table>', '</table>', 1],
@@ -2532,7 +2510,6 @@ Element._insertionTranslations = {
};
(function() {
- this.bottom.initializeRange = this.top.initializeRange;
Object.extend(this.tags, {
THEAD: this.tags.TBODY,
TFOOT: this.tags.TBODY,
@@ -2716,7 +2693,7 @@ document.viewport = {
window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
}
};
-/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
* part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
* license. Please see http://www.yui-ext.com/ for more information. */
@@ -2959,13 +2936,13 @@ Object.extend(Selector, {
},
criteria: {
- tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
- className: 'n = h.className(n, r, "#{1}", c); c = false;',
- id: 'n = h.id(n, r, "#{1}", c); c = false;',
- attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
+ tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
+ className: 'n = h.className(n, r, "#{1}", c); c = false;',
+ id: 'n = h.id(n, r, "#{1}", c); c = false;',
+ attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
attr: function(m) {
m[3] = (m[5] || m[6]);
- return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
+ return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
},
pseudo: function(m) {
if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
@@ -2989,7 +2966,8 @@ Object.extend(Selector, {
tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
id: /^#([\w\-\*]+)(\b|$)/,
className: /^\.([\w\-\*]+)(\b|$)/,
- pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/,
+ pseudo:
+/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
attrPresence: /^\[([\w]+)\]/,
attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
},
@@ -3014,7 +2992,7 @@ Object.extend(Selector, {
attr: function(element, matches) {
var nodeValue = Element.readAttribute(element, matches[1]);
- return Selector.operators[matches[2]](nodeValue, matches[3]);
+ return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
}
},
@@ -3029,14 +3007,15 @@ Object.extend(Selector, {
// marks an array of nodes for counting
mark: function(nodes) {
+ var _true = Prototype.emptyFunction;
for (var i = 0, node; node = nodes[i]; i++)
- node._counted = true;
+ node._countedByPrototype = _true;
return nodes;
},
unmark: function(nodes) {
for (var i = 0, node; node = nodes[i]; i++)
- node._counted = undefined;
+ node._countedByPrototype = undefined;
return nodes;
},
@@ -3044,15 +3023,15 @@ Object.extend(Selector, {
// "ofType" flag indicates whether we're indexing for nth-of-type
// rather than nth-child
index: function(parentNode, reverse, ofType) {
- parentNode._counted = true;
+ parentNode._countedByPrototype = Prototype.emptyFunction;
if (reverse) {
for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
var node = nodes[i];
- if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
}
} else {
for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
- if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
}
},
@@ -3061,8 +3040,8 @@ Object.extend(Selector, {
if (nodes.length == 0) return nodes;
var results = [], n;
for (var i = 0, l = nodes.length; i < l; i++)
- if (!(n = nodes[i])._counted) {
- n._counted = true;
+ if (!(n = nodes[i])._countedByPrototype) {
+ n._countedByPrototype = Prototype.emptyFunction;
results.push(Element.extend(n));
}
return Selector.handlers.unmark(results);
@@ -3114,7 +3093,7 @@ Object.extend(Selector, {
// TOKEN FUNCTIONS
tagName: function(nodes, root, tagName, combinator) {
- tagName = tagName.toUpperCase();
+ var uTagName = tagName.toUpperCase();
var results = [], h = Selector.handlers;
if (nodes) {
if (combinator) {
@@ -3127,7 +3106,7 @@ Object.extend(Selector, {
if (tagName == "*") return nodes;
}
for (var i = 0, node; node = nodes[i]; i++)
- if (node.tagName.toUpperCase() == tagName) results.push(node);
+ if (node.tagName.toUpperCase() === uTagName) results.push(node);
return results;
} else return root.getElementsByTagName(tagName);
},
@@ -3174,16 +3153,18 @@ Object.extend(Selector, {
return results;
},
- attrPresence: function(nodes, root, attr) {
+ attrPresence: function(nodes, root, attr, combinator) {
if (!nodes) nodes = root.getElementsByTagName("*");
+ if (nodes && combinator) nodes = this[combinator](nodes);
var results = [];
for (var i = 0, node; node = nodes[i]; i++)
if (Element.hasAttribute(node, attr)) results.push(node);
return results;
},
- attr: function(nodes, root, attr, value, operator) {
+ attr: function(nodes, root, attr, value, operator, combinator) {
if (!nodes) nodes = root.getElementsByTagName("*");
+ if (nodes && combinator) nodes = this[combinator](nodes);
var handler = Selector.operators[operator], results = [];
for (var i = 0, node; node = nodes[i]; i++) {
var nodeValue = Element.readAttribute(node, attr);
@@ -3262,7 +3243,7 @@ Object.extend(Selector, {
var h = Selector.handlers, results = [], indexed = [], m;
h.mark(nodes);
for (var i = 0, node; node = nodes[i]; i++) {
- if (!node.parentNode._counted) {
+ if (!node.parentNode._countedByPrototype) {
h.index(node.parentNode, reverse, ofType);
indexed.push(node.parentNode);
}
@@ -3300,7 +3281,7 @@ Object.extend(Selector, {
var exclusions = new Selector(selector).findElements(root);
h.mark(exclusions);
for (var i = 0, results = [], node; node = nodes[i]; i++)
- if (!node._counted) results.push(node);
+ if (!node._countedByPrototype) results.push(node);
h.unmark(exclusions);
return results;
},
@@ -3334,11 +3315,19 @@ Object.extend(Selector, {
'|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
},
+ split: function(expression) {
+ var expressions = [];
+ expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+ expressions.push(m[1].strip());
+ });
+ return expressions;
+ },
+
matchElements: function(elements, expression) {
- var matches = new Selector(expression).findElements(), h = Selector.handlers;
+ var matches = $$(expression), h = Selector.handlers;
h.mark(matches);
for (var i = 0, results = [], element; element = elements[i]; i++)
- if (element._counted) results.push(element);
+ if (element._countedByPrototype) results.push(element);
h.unmark(matches);
return results;
},
@@ -3351,11 +3340,7 @@ Object.extend(Selector, {
},
findChildElements: function(element, expressions) {
- var exprs = expressions.join(',');
- expressions = [];
- exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
- expressions.push(m[1].strip());
- });
+ expressions = Selector.split(expressions.join(','));
var results = [], h = Selector.handlers;
for (var i = 0, l = expressions.length, selector; i < l; i++) {
selector = new Selector(expressions[i].strip());
@@ -3366,13 +3351,22 @@ Object.extend(Selector, {
});
if (Prototype.Browser.IE) {
- // IE returns comment nodes on getElementsByTagName("*").
- // Filter them out.
- Selector.handlers.concat = function(a, b) {
- for (var i = 0, node; node = b[i]; i++)
- if (node.tagName !== "!") a.push(node);
- return a;
- };
+ Object.extend(Selector.handlers, {
+ // IE returns comment nodes on getElementsByTagName("*").
+ // Filter them out.
+ concat: function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ if (node.tagName !== "!") a.push(node);
+ return a;
+ },
+
+ // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
+ unmark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node.removeAttribute('_countedByPrototype');
+ return nodes;
+ }
+ });
}
function $$() {
@@ -3850,9 +3844,9 @@ Object.extend(Event, (function() {
var cache = Event.cache;
function getEventID(element) {
- if (element._eventID) return element._eventID;
+ if (element._prototypeEventID) return element._prototypeEventID[0];
arguments.callee.id = arguments.callee.id || 1;
- return element._eventID = ++arguments.callee.id;
+ return element._prototypeEventID = [++arguments.callee.id];
}
function getDOMEventName(eventName) {
@@ -3880,7 +3874,7 @@ Object.extend(Event, (function() {
return false;
Event.extend(event);
- handler.call(element, event)
+ handler.call(element, event);
};
wrapper.handler = handler;
@@ -3962,11 +3956,12 @@ Object.extend(Event, (function() {
if (element == document && document.createEvent && !element.dispatchEvent)
element = document.documentElement;
+ var event;
if (document.createEvent) {
- var event = document.createEvent("HTMLEvents");
+ event = document.createEvent("HTMLEvents");
event.initEvent("dataavailable", true, true);
} else {
- var event = document.createEventObject();
+ event = document.createEventObject();
event.eventType = "ondataavailable";
}
@@ -3995,20 +3990,21 @@ Element.addMethods({
Object.extend(document, {
fire: Element.Methods.fire.methodize(),
observe: Element.Methods.observe.methodize(),
- stopObserving: Element.Methods.stopObserving.methodize()
+ stopObserving: Element.Methods.stopObserving.methodize(),
+ loaded: false
});
(function() {
/* Support for the DOMContentLoaded event is based on work by Dan Webb,
Matthias Miller, Dean Edwards and John Resig. */
- var timer, fired = false;
+ var timer;
function fireContentLoadedEvent() {
- if (fired) return;
+ if (document.loaded) return;
if (timer) window.clearInterval(timer);
document.fire("dom:loaded");
- fired = true;
+ document.loaded = true;
}
if (document.addEventListener) {
diff --git a/railties/lib/commands/dbconsole.rb b/railties/lib/commands/dbconsole.rb
index 17acb7b68f..5be3b5dd8e 100644
--- a/railties/lib/commands/dbconsole.rb
+++ b/railties/lib/commands/dbconsole.rb
@@ -41,11 +41,13 @@ when "mysql"
if config['password'] && include_password
args << "--password=#{config['password']}"
+ elsif config['password'] && !config['password'].empty?
+ args << "-p"
end
args << config['database']
- exec(find_cmd('mysql5', 'mysql'), *args)
+ exec(find_cmd('mysql', 'mysql5'), *args)
when "postgresql"
ENV['PGUSER'] = config["username"] if config["username"]
diff --git a/railties/lib/commands/ncgi/listener b/railties/lib/commands/ncgi/listener
index 421c453f23..7079ef78a6 100644..100755
--- a/railties/lib/commands/ncgi/listener
+++ b/railties/lib/commands/ncgi/listener
@@ -1,4 +1,4 @@
-#!/usr/local/bin/ruby
+#!/usr/bin/env ruby
require 'stringio'
require 'fileutils'
@@ -83,4 +83,4 @@ end
socket_path = ARGV.shift
timeout = (ARGV.shift || 90).to_i
-Listener.new(timeout, socket_path) \ No newline at end of file
+Listener.new(timeout, socket_path)
diff --git a/railties/lib/commands/ncgi/tracker b/railties/lib/commands/ncgi/tracker
index 859c9fa0e0..4ca12d779b 100644..100755
--- a/railties/lib/commands/ncgi/tracker
+++ b/railties/lib/commands/ncgi/tracker
@@ -1,4 +1,4 @@
-#!/usr/local/bin/ruby
+#!/usr/bin/env ruby
require 'drb'
require 'thread'
@@ -66,4 +66,4 @@ end
socket_path = ARGV.shift
instances = ARGV.shift.to_i
t = Tracker.new(instances, socket_path)
-t.background(ARGV.first ? ARGV.shift.to_i : 90) \ No newline at end of file
+t.background(ARGV.first ? ARGV.shift.to_i : 90)
diff --git a/railties/lib/commands/plugin.rb b/railties/lib/commands/plugin.rb
index 0256090d16..980244a71b 100644
--- a/railties/lib/commands/plugin.rb
+++ b/railties/lib/commands/plugin.rb
@@ -907,7 +907,7 @@ class RecursiveHTTPFetcher
def ls
@urls_to_fetch.collect do |url|
- if url =~ /^svn:\/\/.*/
+ if url =~ /^svn(\+ssh)?:\/\/.*/
`svn ls #{url}`.split("\n").map {|entry| "/#{entry}"} rescue nil
else
open(url) do |stream|
diff --git a/railties/lib/commands/process/spawner.rb b/railties/lib/commands/process/spawner.rb
index fd09daa55b..dc0008698a 100644
--- a/railties/lib/commands/process/spawner.rb
+++ b/railties/lib/commands/process/spawner.rb
@@ -66,9 +66,9 @@ class MongrelSpawner < Spawner
"-l #{OPTIONS[:rails_root]}/log/mongrel.log"
# Add prefix functionality to spawner's call to mongrel_rails
- # Digging through monrel's project subversion server, the earliest
+ # Digging through mongrel's project subversion server, the earliest
# Tag that has prefix implemented in the bin/mongrel_rails file
- # is 0.3.15 which also happens to be the earilest tag listed.
+ # is 0.3.15 which also happens to be the earliest tag listed.
# References: http://mongrel.rubyforge.org/svn/tags
if Mongrel::Const::MONGREL_VERSION.to_f >=0.3 && !OPTIONS[:prefix].nil?
cmd = cmd + " --prefix #{OPTIONS[:prefix]}"
diff --git a/railties/lib/commands/runner.rb b/railties/lib/commands/runner.rb
index 926bc26344..14159c3893 100644
--- a/railties/lib/commands/runner.rb
+++ b/railties/lib/commands/runner.rb
@@ -42,7 +42,7 @@ if code_or_file.nil?
$stderr.puts "Run '#{$0} -h' for help."
exit 1
elsif File.exist?(code_or_file)
- eval(File.read(code_or_file))
+ eval(File.read(code_or_file), nil, code_or_file)
else
eval(code_or_file)
end
diff --git a/railties/lib/commands/server.rb b/railties/lib/commands/server.rb
index 7306c248fb..15f417b5be 100644
--- a/railties/lib/commands/server.rb
+++ b/railties/lib/commands/server.rb
@@ -23,10 +23,10 @@ server = case ARGV.first
when "lighttpd", "mongrel", "new_mongrel", "webrick", "thin"
ARGV.shift
else
- if defined?(Thin)
- "thin"
- elsif defined?(Mongrel)
+ if defined?(Mongrel)
"mongrel"
+ elsif defined?(Thin)
+ "thin"
elsif RUBY_PLATFORM !~ /(:?mswin|mingw)/ && !silence_stderr { `lighttpd -version` }.blank? && defined?(FCGI)
"lighttpd"
else
diff --git a/railties/lib/fcgi_handler.rb b/railties/lib/fcgi_handler.rb
index 722aa1940c..1bb55b9275 100644
--- a/railties/lib/fcgi_handler.rb
+++ b/railties/lib/fcgi_handler.rb
@@ -18,7 +18,6 @@ class RailsFCGIHandler
attr_accessor :log_file_path
attr_accessor :gc_request_period
-
# Initialize and run the FastCGI instance, passing arguments through to new.
def self.process!(*args, &block)
new(*args, &block).process!
@@ -68,7 +67,6 @@ class RailsFCGIHandler
end
end
-
protected
def process_each_request(provider)
cgi = nil
@@ -197,7 +195,7 @@ class RailsFCGIHandler
# close resources as they won't be closed by
# the OS when using exec
logger.close rescue nil
- RAILS_DEFAULT_LOGGER.close rescue nil
+ Rails.logger.close rescue nil
exec(command_line)
end
diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb
index 18bcf69d69..008f1de8fa 100644
--- a/railties/lib/initializer.rb
+++ b/railties/lib/initializer.rb
@@ -33,7 +33,11 @@ module Rails
end
def logger
- RAILS_DEFAULT_LOGGER
+ if defined?(RAILS_DEFAULT_LOGGER)
+ RAILS_DEFAULT_LOGGER
+ else
+ nil
+ end
end
def root
@@ -45,6 +49,7 @@ module Rails
end
def env
+ require 'active_support/string_inquirer'
ActiveSupport::StringInquirer.new(RAILS_ENV)
end
@@ -98,7 +103,7 @@ module Rails
# Rails::Initializer.run(:set_load_path)
#
# This is useful if you only want the load path initialized, without
- # incuring the overhead of completely loading the entire environment.
+ # incurring the overhead of completely loading the entire environment.
def self.run(command = :process, configuration = Configuration.new)
yield configuration if block_given?
initializer = new configuration
@@ -168,6 +173,15 @@ module Rails
# Observers are loaded after plugins in case Observers or observed models are modified by plugins.
load_observers
+ # Load view path cache
+ load_view_paths
+
+ # Load application classes
+ load_application_classes
+
+ # Disable dependency loading during request cycle
+ disable_dependency_loading
+
# Flag initialized
Rails.initialized = true
end
@@ -266,11 +280,16 @@ module Rails
@gems_dependencies_loaded = false
# don't print if the gems rake tasks are being run
unless $rails_gem_installer
- puts %{These gems that this application depends on are missing:}
- unloaded_gems.each do |gem|
- puts " - #{gem.name}"
- end
- puts %{Run "rake gems:install" to install them.}
+ abort <<-end_error
+Missing these required gems:
+ #{unloaded_gems.map { |gem| "#{gem.name} #{gem.requirement}" } * "\n "}
+
+You're running:
+ ruby #{Gem.ruby_version} at #{Gem.ruby}
+ rubygems #{Gem::RubyGemsVersion} at #{Gem.path * ', '}
+
+Run `rake gems:install` to install the missing gems.
+ end_error
end
else
@gems_dependencies_loaded = true
@@ -325,6 +344,26 @@ module Rails
end
end
+ def load_view_paths
+ if configuration.frameworks.include?(:action_view)
+ ActionView::PathSet::Path.eager_load_templates! if configuration.cache_classes
+ ActionController::Base.view_paths.load if configuration.frameworks.include?(:action_controller)
+ ActionMailer::Base.template_root.load if configuration.frameworks.include?(:action_mailer)
+ end
+ end
+
+ # Eager load application classes
+ def load_application_classes
+ if configuration.cache_classes
+ configuration.eager_load_paths.each do |load_path|
+ matcher = /\A#{Regexp.escape(load_path)}(.*)\.rb\Z/
+ Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
+ require_dependency file.sub(matcher, '\1')
+ end
+ end
+ end
+ end
+
# For Ruby 1.8, this initialization sets $KCODE to 'u' to enable the
# multibyte safe operations. Plugin authors supporting other encodings
# should override this behaviour and set the relevant +default_charset+
@@ -369,7 +408,7 @@ module Rails
# +STDERR+, with a log level of +WARN+.
def initialize_logger
# if the environment has explicitly defined a logger, use it
- return if defined?(RAILS_DEFAULT_LOGGER)
+ return if Rails.logger
unless logger = configuration.logger
begin
@@ -377,7 +416,6 @@ module Rails
logger.level = ActiveSupport::BufferedLogger.const_get(configuration.log_level.to_s.upcase)
if configuration.environment == "production"
logger.auto_flushing = false
- logger.set_non_blocking_io
end
rescue StandardError => e
logger = ActiveSupport::BufferedLogger.new(STDERR)
@@ -398,10 +436,11 @@ module Rails
# RAILS_DEFAULT_LOGGER.
def initialize_framework_logging
for framework in ([ :active_record, :action_controller, :action_mailer ] & configuration.frameworks)
- framework.to_s.camelize.constantize.const_get("Base").logger ||= RAILS_DEFAULT_LOGGER
+ framework.to_s.camelize.constantize.const_get("Base").logger ||= Rails.logger
end
- RAILS_CACHE.logger ||= RAILS_DEFAULT_LOGGER
+ ActiveSupport::Dependencies.logger ||= Rails.logger
+ Rails.cache.logger ||= Rails.logger
end
# Sets +ActionController::Base#view_paths+ and +ActionMailer::Base#template_root+
@@ -409,8 +448,11 @@ module Rails
# paths have already been set, it is not changed, otherwise it is
# set to use Configuration#view_path.
def initialize_framework_views
- ActionMailer::Base.template_root ||= configuration.view_path if configuration.frameworks.include?(:action_mailer)
- ActionController::Base.view_paths = [configuration.view_path] if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.empty?
+ if configuration.frameworks.include?(:action_view)
+ view_path = ActionView::PathSet::Path.new(configuration.view_path, false)
+ ActionMailer::Base.template_root ||= view_path if configuration.frameworks.include?(:action_mailer)
+ ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.empty?
+ end
end
# If Action Controller is not one of the loaded frameworks (Configuration#frameworks)
@@ -492,9 +534,16 @@ module Rails
end
def prepare_dispatcher
+ return unless configuration.frameworks.include?(:action_controller)
require 'dispatcher' unless defined?(::Dispatcher)
Dispatcher.define_dispatcher_callbacks(configuration.cache_classes)
- Dispatcher.new(RAILS_DEFAULT_LOGGER).send :run_callbacks, :prepare_dispatch
+ Dispatcher.new(Rails.logger).send :run_callbacks, :prepare_dispatch
+ end
+
+ def disable_dependency_loading
+ if configuration.cache_classes && !configuration.dependency_loading
+ ActiveSupport::Dependencies.unhook!
+ end
end
end
@@ -523,7 +572,7 @@ module Rails
# A stub for setting options on ActiveRecord::Base.
attr_accessor :active_record
- # A stub for setting options on ActiveRecord::Base.
+ # A stub for setting options on ActiveResource::Base.
attr_accessor :active_resource
# A stub for setting options on ActiveSupport.
@@ -559,6 +608,11 @@ module Rails
# All elements of this array must also be in +load_paths+.
attr_accessor :load_once_paths
+ # An array of paths from which Rails will eager load on boot if cache
+ # classes is enabled. All elements of this array must also be in
+ # +load_paths+.
+ attr_accessor :eager_load_paths
+
# The log level to use for the default Rails logger. In production mode,
# this defaults to <tt>:info</tt>. In development mode, it defaults to
# <tt>:debug</tt>.
@@ -625,6 +679,17 @@ module Rails
!!@reload_plugins
end
+ # Enables or disables dependency loading during the request cycle. Setting
+ # <tt>dependency_loading</tt> to true will allow new classes to be loaded
+ # during a request. Setting it to false will disable this behavior.
+ #
+ # Those who want to run in a threaded environment should disable this
+ # option and eager load or require all there classes on initialization.
+ #
+ # If <tt>cache_classes</tt> is disabled, dependency loaded will always be
+ # on.
+ attr_accessor :dependency_loading
+
# An array of gems that this rails application depends on. Rails will automatically load
# these gems during installation, and allow you to install any missing gems with:
#
@@ -633,13 +698,17 @@ module Rails
# You can add gems with the #gem method.
attr_accessor :gems
- # Adds a single Gem dependency to the rails application.
+ # Adds a single Gem dependency to the rails application. By default, it will require
+ # the library with the same name as the gem. Use :lib to specify a different name.
#
# # gem 'aws-s3', '>= 0.4.0'
# # require 'aws/s3'
# config.gem 'aws-s3', :lib => 'aws/s3', :version => '>= 0.4.0', \
# :source => "http://code.whytheluckystiff.net"
#
+ # To require a library be installed, but not attempt to load it, pass :lib => false
+ #
+ # config.gem 'qrp', :version => '0.4.1', :lib => false
def gem(name, options = {})
@gems << Rails::GemDependency.new(name, options)
end
@@ -667,11 +736,13 @@ module Rails
self.frameworks = default_frameworks
self.load_paths = default_load_paths
self.load_once_paths = default_load_once_paths
+ self.eager_load_paths = default_eager_load_paths
self.log_path = default_log_path
self.log_level = default_log_level
self.view_path = default_view_path
self.controller_paths = default_controller_paths
self.cache_classes = default_cache_classes
+ self.dependency_loading = default_dependency_loading
self.whiny_nils = default_whiny_nils
self.plugins = default_plugins
self.plugin_paths = default_plugin_paths
@@ -707,10 +778,22 @@ module Rails
::RAILS_ROOT.replace @root_path
end
+ # Enable threaded mode. Allows concurrent requests to controller actions and
+ # multiple database connections. Also disables automatic dependency loading
+ # after boot
+ def threadsafe!
+ self.cache_classes = true
+ self.dependency_loading = false
+ self.active_record.allow_concurrency = true
+ self.action_controller.allow_concurrency = true
+ self
+ end
+
# Loads and returns the contents of the #database_configuration_file. The
# contents of the file are processed via ERB before being sent through
# YAML::load.
def database_configuration
+ require 'erb'
YAML::load(ERB.new(IO.read(database_configuration_file)).result)
end
@@ -807,6 +890,14 @@ module Rails
[]
end
+ def default_eager_load_paths
+ %w(
+ app/models
+ app/controllers
+ app/helpers
+ ).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) }
+ end
+
def default_log_path
File.join(root_path, 'log', "#{environment}.log")
end
@@ -833,12 +924,12 @@ module Rails
paths
end
- def default_dependency_mechanism
- :load
+ def default_dependency_loading
+ true
end
def default_cache_classes
- false
+ true
end
def default_whiny_nils
diff --git a/railties/lib/performance_test_help.rb b/railties/lib/performance_test_help.rb
new file mode 100644
index 0000000000..5148b4ab77
--- /dev/null
+++ b/railties/lib/performance_test_help.rb
@@ -0,0 +1,5 @@
+require 'action_controller/performance_test'
+
+ActionController::Base.perform_caching = true
+ActiveSupport::Dependencies.mechanism = :require
+Rails.logger.level = ActiveSupport::BufferedLogger::INFO
diff --git a/railties/lib/rails/gem_dependency.rb b/railties/lib/rails/gem_dependency.rb
index f8d97840c1..471e03fa5f 100644
--- a/railties/lib/rails/gem_dependency.rb
+++ b/railties/lib/rails/gem_dependency.rb
@@ -58,7 +58,7 @@ module Rails
def load
return if @loaded || @load_paths_added == false
- require(@lib || @name)
+ require(@lib || @name) unless @lib == false
@loaded = true
rescue LoadError
puts $!.to_s
diff --git a/railties/lib/rails/rack.rb b/railties/lib/rails/rack.rb
index abcd0741bf..b4f32c2d95 100644
--- a/railties/lib/rails/rack.rb
+++ b/railties/lib/rails/rack.rb
@@ -1,5 +1,6 @@
module Rails
module Rack
+ autoload :Logger, "rails/rack/logger"
autoload :Static, "rails/rack/static"
end
end
diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb
new file mode 100644
index 0000000000..89d02e45a9
--- /dev/null
+++ b/railties/lib/rails/rack/logger.rb
@@ -0,0 +1,28 @@
+module Rails
+ module Rack
+ class Logger
+ EnvironmentLog = "#{File.expand_path(Rails.root)}/log/#{Rails.env}.log"
+
+ def initialize(app, log = nil)
+ @app = app
+ @path = Pathname.new(log || EnvironmentLog).cleanpath
+ @cursor = ::File.size(@path)
+ @last_checked = Time.now
+ end
+
+ def call(env)
+ response = @app.call(env)
+ ::File.open(@path, 'r') do |f|
+ f.seek @cursor
+ if f.mtime > @last_checked
+ contents = f.read
+ @last_checked = f.mtime
+ @cursor += contents.length
+ print contents
+ end
+ end
+ response
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails_generator/commands.rb b/railties/lib/rails_generator/commands.rb
index d258aeaa0a..59af7308fe 100644
--- a/railties/lib/rails_generator/commands.rb
+++ b/railties/lib/rails_generator/commands.rb
@@ -57,6 +57,17 @@ module Rails
end
protected
+ def current_migration_number
+ Dir.glob("#{RAILS_ROOT}/#{@migration_directory}/[0-9]*_*.rb").inject(0) do |max, file_path|
+ n = File.basename(file_path).split('_', 2).first.to_i
+ if n > max then n else max end
+ end
+ end
+
+ def next_migration_number
+ current_migration_number + 1
+ end
+
def migration_directory(relative_path)
directory(@migration_directory = relative_path)
end
@@ -70,7 +81,11 @@ module Rails
end
def next_migration_string(padding = 3)
- Time.now.utc.strftime("%Y%m%d%H%M%S")
+ if ActiveRecord::Base.timestamped_migrations
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
+ else
+ "%.#{padding}d" % next_migration_number
+ end
end
def gsub_file(relative_destination, regexp, *args, &block)
diff --git a/railties/lib/rails_generator/generated_attribute.rb b/railties/lib/rails_generator/generated_attribute.rb
index 25af3931de..a3d4a01142 100644
--- a/railties/lib/rails_generator/generated_attribute.rb
+++ b/railties/lib/rails_generator/generated_attribute.rb
@@ -37,6 +37,10 @@ module Rails
""
end
end
+
+ def reference?
+ [ :references, :belongs_to ].include?(self.type)
+ end
end
end
end
diff --git a/railties/lib/rails_generator/generators/components/model/templates/model.rb b/railties/lib/rails_generator/generators/components/model/templates/model.rb
index 8d4c89e912..6fcf393bdf 100644
--- a/railties/lib/rails_generator/generators/components/model/templates/model.rb
+++ b/railties/lib/rails_generator/generators/components/model/templates/model.rb
@@ -1,2 +1,5 @@
class <%= class_name %> < ActiveRecord::Base
+<% attributes.select(&:reference?).each do |attribute| -%>
+ belongs_to :<%= attribute.name %>
+<% end -%>
end
diff --git a/railties/lib/rails_generator/generators/components/plugin/templates/Rakefile b/railties/lib/rails_generator/generators/components/plugin/templates/Rakefile
index 1824fb10fe..1824fb10fe 100755..100644
--- a/railties/lib/rails_generator/generators/components/plugin/templates/Rakefile
+++ b/railties/lib/rails_generator/generators/components/plugin/templates/Rakefile
diff --git a/railties/lib/rails_generator/generators/components/scaffold/USAGE b/railties/lib/rails_generator/generators/components/scaffold/USAGE
index a0e4baea08..810aea16f1 100644
--- a/railties/lib/rails_generator/generators/components/scaffold/USAGE
+++ b/railties/lib/rails_generator/generators/components/scaffold/USAGE
@@ -1,10 +1,11 @@
Description:
Scaffolds an entire resource, from model and migration to controller and
views, along with a full test suite. The resource is ready to use as a
- starting point for your restful, resource-oriented application.
+ starting point for your RESTful, resource-oriented application.
- Pass the name of the model, either CamelCased or under_scored, as the first
- argument, and an optional list of attribute pairs.
+ Pass the name of the model (in singular form), either CamelCased or
+ under_scored, as the first argument, and an optional list of attribute
+ pairs.
Attribute pairs are column_name:sql_type arguments specifying the
model's attributes. Timestamps are added by default, so you don't have to
@@ -13,13 +14,16 @@ Description:
You don't have to think up every attribute up front, but it helps to
sketch out a few so you can start working with the resource immediately.
- For example, `scaffold post title:string body:text published:boolean`
+ For example, 'scaffold post title:string body:text published:boolean'
gives you a model with those three attributes, a controller that handles
the create/show/update/destroy, forms to create and edit your posts, and
an index that lists them all, as well as a map.resources :posts
declaration in config/routes.rb.
+ If you want to remove all the generated files, run
+ 'script/destroy scaffold ModelName'.
+
Examples:
- `./script/generate scaffold post` # no attributes, view will be anemic
+ `./script/generate scaffold post`
`./script/generate scaffold post title:string body:text published:boolean`
`./script/generate scaffold purchase order_id:integer amount:decimal`
diff --git a/railties/lib/rails_generator/scripts.rb b/railties/lib/rails_generator/scripts.rb
index f857f68de4..9b1a99838a 100644
--- a/railties/lib/rails_generator/scripts.rb
+++ b/railties/lib/rails_generator/scripts.rb
@@ -45,7 +45,7 @@ module Rails
usage = "\nInstalled Generators\n"
Rails::Generator::Base.sources.inject([]) do |mem, source|
# Using an association list instead of a hash to preserve order,
- # for esthetic reasons more than anything else.
+ # for aesthetic reasons more than anything else.
label = source.label.to_s.capitalize
pair = mem.assoc(label)
mem << (pair = [label, []]) if pair.nil?
diff --git a/railties/lib/rails_generator/scripts/destroy.rb b/railties/lib/rails_generator/scripts/destroy.rb
index 4fcbc3e0df..a7c2a14751 100644
--- a/railties/lib/rails_generator/scripts/destroy.rb
+++ b/railties/lib/rails_generator/scripts/destroy.rb
@@ -3,7 +3,7 @@ require File.dirname(__FILE__) + '/../scripts'
module Rails::Generator::Scripts
class Destroy < Base
mandatory_options :command => :destroy
-
+
protected
def usage_message
usage = "\nInstalled Generators\n"
@@ -15,14 +15,13 @@ module Rails::Generator::Scripts
usage << <<end_blurb
-This script will destroy all files created by the corresponding
-script/generate command. For instance, script/destroy migration CreatePost
-will delete the appropriate ###_create_post.rb file in db/migrate, while
-script/destroy scaffold Post will delete the posts controller and
+script/generate command. For instance, 'script/destroy migration CreatePost'
+will delete the appropriate XXX_create_post.rb migration file in db/migrate,
+while 'script/destroy scaffold Post' will delete the posts controller and
views, post model and migration, all associated tests, and the map.resources
:posts line in config/routes.rb.
-
-For instructions on finding new generators, run script/generate
+
+For instructions on finding new generators, run script/generate.
end_blurb
return usage
end
diff --git a/railties/lib/rails_generator/secret_key_generator.rb b/railties/lib/rails_generator/secret_key_generator.rb
index 64fbbb90f8..5ae492312e 100644
--- a/railties/lib/rails_generator/secret_key_generator.rb
+++ b/railties/lib/rails_generator/secret_key_generator.rb
@@ -23,7 +23,7 @@ module Rails
# Generate a random secret key by using the Win32 API. Raises LoadError
# if the current platform cannot make use of the Win32 API. Raises
- # SystemCallError if some other error occured.
+ # SystemCallError if some other error occurred.
def generate_secret_with_win32_api
# Following code is based on David Garamond's GUID library for Ruby.
require 'Win32API'
diff --git a/railties/lib/tasks/databases.rake b/railties/lib/tasks/databases.rake
index 5ec712a02d..21c81b3fb5 100644
--- a/railties/lib/tasks/databases.rake
+++ b/railties/lib/tasks/databases.rake
@@ -182,11 +182,11 @@ namespace :db do
end
namespace :fixtures do
- desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z."
+ desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
task :load => :environment do
require 'active_record/fixtures'
ActiveRecord::Base.establish_connection(Rails.env)
- base_dir = File.join(Rails.root, 'test', 'fixtures')
+ base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures')
fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir
(ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir.glob(File.join(fixtures_dir, '*.{yml,csv}'))).each do |fixture_file|
@@ -194,7 +194,7 @@ namespace :db do
end
end
- desc "Search for a fixture given a LABEL or ID."
+ desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
task :identify => :environment do
require "active_record/fixtures"
@@ -203,7 +203,8 @@ namespace :db do
puts %Q(The fixture ID for "#{label}" is #{Fixtures.identify(label)}.) if label
- Dir["#{RAILS_ROOT}/test/fixtures/**/*.yml"].each do |file|
+ base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures')
+ Dir["#{base_dir}/**/*.yml"].each do |file|
if data = YAML::load(ERB.new(IO.read(file)).result)
data.keys.each do |key|
key_id = Fixtures.identify(key)
diff --git a/railties/lib/tasks/documentation.rake b/railties/lib/tasks/documentation.rake
index 331b2450a3..f4927a0ef1 100644
--- a/railties/lib/tasks/documentation.rake
+++ b/railties/lib/tasks/documentation.rake
@@ -62,6 +62,7 @@ namespace :doc do
options << "-o doc/plugins/#{plugin}"
options << "--title '#{plugin.titlecase} Plugin Documentation'"
options << '--line-numbers' << '--inline-source'
+ options << '--charset' << 'utf-8'
options << '-T html'
files.include("#{plugin_base}/lib/**/*.rb")
diff --git a/railties/lib/tasks/framework.rake b/railties/lib/tasks/framework.rake
index 71aea09867..66ab78c3b2 100644
--- a/railties/lib/tasks/framework.rake
+++ b/railties/lib/tasks/framework.rake
@@ -43,9 +43,12 @@ namespace :rails do
require 'open-uri'
version = ENV["RELEASE"] || "edge"
target = "rails_#{version}.zip"
+ commits = "http://github.com/api/v1/yaml/rails/rails/commits/master"
url = "http://dev.rubyonrails.org/archives/#{target}"
chdir 'vendor' do
+ latest_revision = YAML.load(open(commits))["commits"].first["id"]
+
puts "Downloading Rails from #{url}"
File.open('rails.zip', 'wb') do |dst|
open url do |src|
@@ -61,6 +64,8 @@ namespace :rails do
%w(rails.zip rails/Rakefile rails/cleanlogs.sh rails/pushgems.rb rails/release.rb).each do |goner|
rm_f goner
end
+
+ touch "rails/REVISION_#{latest_revision}"
end
puts 'Updating current scripts, javascripts, and configuration settings'
diff --git a/railties/test/error_page_test.rb b/railties/test/error_page_test.rb
new file mode 100644
index 0000000000..844f889aad
--- /dev/null
+++ b/railties/test/error_page_test.rb
@@ -0,0 +1,43 @@
+require 'abstract_unit'
+require 'action_controller'
+require 'action_controller/test_process'
+
+RAILS_ENV = "test"
+CURRENT_DIR = File.expand_path(File.dirname(__FILE__))
+HTML_DIR = File.expand_path(File.join(CURRENT_DIR, "..", "html"))
+
+module Rails
+ def self.public_path
+ CURRENT_DIR
+ end
+end
+
+class ErrorPageController < ActionController::Base
+ def crash
+ raise StandardError, "crash!"
+ end
+end
+
+ActionController::Routing::Routes.draw do |map|
+ map.connect ':controller/:action/:id'
+end
+
+class ErrorPageControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = ErrorPageController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+
+ ActionController::Base.consider_all_requests_local = false
+ end
+
+ def test_500_error_page_instructs_system_administrator_to_check_log_file
+ template = ERB.new(File.read(File.join(HTML_DIR, "500.html")))
+ File.open(File.join(CURRENT_DIR, "500.html"), "w") do |f|
+ f.write(template.result)
+ end
+ get :crash
+ expected_log_file = "#{RAILS_ENV}.log"
+ assert_not_nil @response.body.index(expected_log_file)
+ end
+end
diff --git a/railties/test/fixtures/eager/zoo.rb b/railties/test/fixtures/eager/zoo.rb
new file mode 100644
index 0000000000..8b10ef984b
--- /dev/null
+++ b/railties/test/fixtures/eager/zoo.rb
@@ -0,0 +1,3 @@
+class Zoo
+ include ReptileHouse
+end \ No newline at end of file
diff --git a/railties/test/fixtures/eager/zoo/reptile_house.rb b/railties/test/fixtures/eager/zoo/reptile_house.rb
new file mode 100644
index 0000000000..82bbafce79
--- /dev/null
+++ b/railties/test/fixtures/eager/zoo/reptile_house.rb
@@ -0,0 +1,2 @@
+module Zoo::ReptileHouse
+end \ No newline at end of file
diff --git a/railties/test/gem_dependency_test.rb b/railties/test/gem_dependency_test.rb
index b5946aa7b8..964ca50992 100644
--- a/railties/test/gem_dependency_test.rb
+++ b/railties/test/gem_dependency_test.rb
@@ -11,6 +11,7 @@ uses_mocha "Plugin Tests" do
@gem_with_source = Rails::GemDependency.new "hpricot", :source => "http://code.whytheluckystiff.net"
@gem_with_version = Rails::GemDependency.new "hpricot", :version => "= 0.6"
@gem_with_lib = Rails::GemDependency.new "aws-s3", :lib => "aws/s3"
+ @gem_without_load = Rails::GemDependency.new "hpricot", :lib => false
end
def test_configuration_adds_gem_dependency
@@ -62,5 +63,13 @@ uses_mocha "Plugin Tests" do
@gem_with_lib.add_load_paths
@gem_with_lib.load
end
+
+ def test_gem_without_lib_loading
+ @gem_without_load.expects(:gem).with(@gem_without_load.name)
+ @gem_without_load.expects(:require).with(@gem_without_load.lib).never
+ @gem_without_load.add_load_paths
+ @gem_without_load.load
+ end
+
end
end
diff --git a/railties/test/generators/generator_test_helper.rb b/railties/test/generators/generator_test_helper.rb
index 05dca3400e..0901b215e4 100644
--- a/railties/test/generators/generator_test_helper.rb
+++ b/railties/test/generators/generator_test_helper.rb
@@ -5,9 +5,10 @@ require 'fileutils'
module ActiveRecord
class Base
class << self
- attr_accessor :pluralize_table_names
+ attr_accessor :pluralize_table_names, :timestamped_migrations
end
self.pluralize_table_names = true
+ self.timestamped_migrations = true
end
module ConnectionAdapters
@@ -226,7 +227,7 @@ class GeneratorTestCase < Test::Unit::TestCase
end
end
- # Asserts that the given fixtures yaml file was generated.
+ # Asserts that the given fixtures YAML file was generated.
# It takes a fixture name without the <tt>.yml</tt> part.
# The parsed YAML tree is passed to a block.
def assert_generated_fixtures_for(name)
diff --git a/railties/test/generators/rails_model_generator_test.rb b/railties/test/generators/rails_model_generator_test.rb
index 0bfc338566..aea2abafba 100644
--- a/railties/test/generators/rails_model_generator_test.rb
+++ b/railties/test/generators/rails_model_generator_test.rb
@@ -29,4 +29,20 @@ class RailsModelGeneratorTest < GeneratorTestCase
assert_generated_column t, :created_at, :timestamp
end
end
+
+ def test_model_with_reference_attributes_generates_belongs_to_associations
+ run_generator('model', %w(Product name:string supplier:references))
+
+ assert_generated_model_for :product do |body|
+ assert body =~ /^\s+belongs_to :supplier/, "#{body.inspect} should contain 'belongs_to :supplier'"
+ end
+ end
+
+ def test_model_with_belongs_to_attributes_generates_belongs_to_associations
+ run_generator('model', %w(Product name:string supplier:belongs_to))
+
+ assert_generated_model_for :product do |body|
+ assert body =~ /^\s+belongs_to :supplier/, "#{body.inspect} should contain 'belongs_to :supplier'"
+ end
+ end
end
diff --git a/railties/test/initializer_test.rb b/railties/test/initializer_test.rb
index dee7abe05f..5147eeb482 100644
--- a/railties/test/initializer_test.rb
+++ b/railties/test/initializer_test.rb
@@ -30,6 +30,24 @@ class Initializer_load_environment_Test < Test::Unit::TestCase
end
+class Initializer_eager_loading_Test < Test::Unit::TestCase
+ def setup
+ @config = ConfigurationMock.new("")
+ @config.cache_classes = true
+ @config.load_paths = [File.expand_path(File.dirname(__FILE__) + "/fixtures/eager")]
+ @config.eager_load_paths = [File.expand_path(File.dirname(__FILE__) + "/fixtures/eager")]
+ @initializer = Rails::Initializer.new(@config)
+ @initializer.set_load_path
+ @initializer.set_autoload_paths
+ end
+
+ def test_eager_loading_loads_parent_classes_before_children
+ assert_nothing_raised do
+ @initializer.load_application_classes
+ end
+ end
+end
+
uses_mocha 'Initializer after_initialize' do
class Initializer_after_initialize_with_blocks_environment_Test < Test::Unit::TestCase
def setup
@@ -136,8 +154,27 @@ uses_mocha 'framework paths' do
end
end
- protected
+ def test_action_mailer_load_paths_set_only_if_action_mailer_in_use
+ @config.frameworks = [:action_controller]
+ initializer = Rails::Initializer.new @config
+ initializer.send :require_frameworks
+ assert_nothing_raised NameError do
+ initializer.send :load_view_paths
+ end
+ end
+
+ def test_action_controller_load_paths_set_only_if_action_controller_in_use
+ @config.frameworks = []
+ initializer = Rails::Initializer.new @config
+ initializer.send :require_frameworks
+
+ assert_nothing_raised NameError do
+ initializer.send :load_view_paths
+ end
+ end
+
+ protected
def assert_framework_path(path)
assert @config.framework_paths.include?(path),
"<#{path.inspect}> not found among <#{@config.framework_paths.inspect}>"