aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRyan Bigg <radarlistener@gmail.com>2009-04-04 16:13:18 +1000
committerRyan Bigg <radarlistener@gmail.com>2009-04-04 16:13:18 +1000
commite878c3e44a8dba19c1188b636e25ec1bc2165116 (patch)
tree3d06b83492653b6b73422cbf37014fc5c89e9685
parentab55ddcc4c5cf31bcaf7720b52dc55d6d54cb150 (diff)
parent9e9469e83f6144310115124cdecc5cb65db5128e (diff)
downloadrails-e878c3e44a8dba19c1188b636e25ec1bc2165116.tar.gz
rails-e878c3e44a8dba19c1188b636e25ec1bc2165116.tar.bz2
rails-e878c3e44a8dba19c1188b636e25ec1bc2165116.zip
Merge branch 'master' of git@github.com:lifo/docrails
* 'master' of git@github.com:lifo/docrails: (319 commits) deletes screencast promo in prologue, its proper place is the References section Typo fix list -> index in caching guide, RESTifies some examples, revised conventions here and there Tech edit of caching guide from Gregg Pollack Fix typo in comment: hide_actions -> hide_action Fix two typos in a comment in config/initializers/backtrace_silencers.rb With -> with in a title Clear up a little confusing wording in Routing Guide. copyedited minor details in the rack on rails guide remove piece of UrlWriter documentation claiming that you can access named routes as its class methods Add note about change to session options TRUNCATE is also a MySQL DDL statement, so document this is a possible caveat when using transactions and savepoints. Improve documentation for ActiveResource::Validations, fix typos Fix typos in ActiveResource::Base documentation, use present tense, reword confusing sentences Update ActiveResource::Connection documentation to use present tense Fix typos in Active Resource README Fix a small typo ensure authors get warnings about broken links, and ensure end users don't in guides generator, warn about duplicate header IDs only if WARN_DUPLICATE_HEADERS ...
-rw-r--r--actionmailer/CHANGELOG5
-rw-r--r--actionmailer/Rakefile2
-rw-r--r--actionmailer/lib/action_mailer.rb3
-rw-r--r--actionmailer/lib/action_mailer/base.rb2
-rw-r--r--actionmailer/lib/action_mailer/part.rb5
-rwxr-xr-xactionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb2
-rw-r--r--actionmailer/lib/action_mailer/version.rb2
-rw-r--r--actionmailer/test/mail_layout_test.rb30
-rw-r--r--actionmailer/test/mail_service_test.rb3
-rw-r--r--actionmailer/test/test_helper_test.rb8
-rw-r--r--actionpack/CHANGELOG13
-rw-r--r--actionpack/Rakefile3
-rw-r--r--actionpack/lib/action_controller.rb7
-rw-r--r--actionpack/lib/action_controller/base.rb68
-rw-r--r--actionpack/lib/action_controller/caching/actions.rb27
-rw-r--r--actionpack/lib/action_controller/dispatcher.rb7
-rw-r--r--actionpack/lib/action_controller/http_authentication.rb72
-rw-r--r--actionpack/lib/action_controller/integration.rb12
-rw-r--r--actionpack/lib/action_controller/layout.rb25
-rw-r--r--actionpack/lib/action_controller/polymorphic_routes.rb7
-rwxr-xr-xactionpack/lib/action_controller/request.rb3
-rw-r--r--actionpack/lib/action_controller/resources.rb15
-rw-r--r--actionpack/lib/action_controller/response.rb41
-rw-r--r--actionpack/lib/action_controller/routing.rb2
-rw-r--r--actionpack/lib/action_controller/routing/builder.rb3
-rw-r--r--actionpack/lib/action_controller/routing/recognition_optimisation.rb1
-rw-r--r--actionpack/lib/action_controller/routing/segments.rb14
-rw-r--r--actionpack/lib/action_controller/test_process.rb4
-rw-r--r--actionpack/lib/action_controller/url_rewriter.rb22
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb2
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack.rb4
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/abstract/handler.rb4
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/digest/md5.rb2
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/digest/request.rb2
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/builder.rb6
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/chunked.rb49
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/content_length.rb8
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/content_type.rb23
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/deflater.rb4
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/directory.rb6
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/file.rb2
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/cgi.rb4
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/fastcgi.rb3
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/lsws.rb7
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/mongrel.rb4
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/scgi.rb4
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/thin.rb3
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/webrick.rb10
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/lint.rb58
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/response.rb2
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/showstatus.rb2
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/urlmap.rb6
-rw-r--r--actionpack/lib/action_controller/vendor/rack-1.0/rack/utils.rb15
-rw-r--r--actionpack/lib/action_pack/version.rb2
-rw-r--r--actionpack/lib/action_view/base.rb30
-rw-r--r--actionpack/lib/action_view/helpers/active_record_helper.rb4
-rw-r--r--actionpack/lib/action_view/helpers/capture_helper.rb8
-rw-r--r--actionpack/lib/action_view/helpers/date_helper.rb4
-rw-r--r--actionpack/lib/action_view/helpers/form_helper.rb167
-rw-r--r--actionpack/lib/action_view/helpers/form_tag_helper.rb4
-rw-r--r--actionpack/lib/action_view/helpers/number_helper.rb6
-rw-r--r--actionpack/lib/action_view/helpers/prototype_helper.rb2
-rw-r--r--actionpack/lib/action_view/helpers/text_helper.rb11
-rw-r--r--actionpack/lib/action_view/paths.rb8
-rw-r--r--actionpack/lib/action_view/renderable.rb26
-rw-r--r--actionpack/lib/action_view/template.rb25
-rw-r--r--actionpack/test/activerecord/active_record_store_test.rb46
-rw-r--r--actionpack/test/controller/assert_select_test.rb61
-rw-r--r--actionpack/test/controller/caching_test.rb14
-rw-r--r--actionpack/test/controller/fake_models.rb8
-rw-r--r--actionpack/test/controller/html-scanner/document_test.rb2
-rw-r--r--actionpack/test/controller/http_digest_authentication_test.rb53
-rw-r--r--actionpack/test/controller/integration_test.rb6
-rw-r--r--actionpack/test/controller/layout_test.rb12
-rw-r--r--actionpack/test/controller/mime_responds_test.rb2
-rw-r--r--actionpack/test/controller/polymorphic_routes_test.rb85
-rw-r--r--actionpack/test/controller/rack_test.rb2
-rw-r--r--actionpack/test/controller/redirect_test.rb4
-rw-r--r--actionpack/test/controller/render_test.rb139
-rw-r--r--actionpack/test/controller/request/multipart_params_parsing_test.rb2
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb18
-rw-r--r--actionpack/test/controller/request_test.rb6
-rw-r--r--actionpack/test/controller/resources_test.rb84
-rw-r--r--actionpack/test/controller/routing_test.rb112
-rw-r--r--actionpack/test/controller/selector_test.rb6
-rw-r--r--actionpack/test/controller/send_file_test.rb6
-rw-r--r--actionpack/test/controller/session/cookie_store_test.rb30
-rw-r--r--actionpack/test/controller/session/mem_cache_store_test.rb40
-rw-r--r--actionpack/test/controller/url_rewriter_test.rb2
-rw-r--r--actionpack/test/fixtures/layouts/default_html.html.erb1
-rw-r--r--actionpack/test/fixtures/layouts/xhr.html.erb2
-rw-r--r--actionpack/test/fixtures/quiz/questions/_question.html.erb1
-rw-r--r--actionpack/test/fixtures/test/malformed/malformed.en.html.erb~1
-rw-r--r--actionpack/test/fixtures/test/malformed/malformed.erb~1
-rw-r--r--actionpack/test/fixtures/test/malformed/malformed.html.erb~1
-rw-r--r--actionpack/test/template/active_record_helper_test.rb34
-rw-r--r--actionpack/test/template/body_parts_test.rb22
-rw-r--r--actionpack/test/template/form_helper_test.rb41
-rw-r--r--actionpack/test/template/form_tag_helper_test.rb11
-rw-r--r--actionpack/test/template/javascript_helper_test.rb5
-rw-r--r--actionpack/test/template/number_helper_test.rb1
-rw-r--r--actionpack/test/template/output_buffer_test.rb35
-rw-r--r--actionpack/test/template/render_test.rb14
-rw-r--r--actionpack/test/template/text_helper_test.rb6
-rw-r--r--actionpack/test/template/url_helper_test.rb2
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb2
-rw-r--r--activemodel/test/state_machine/event_test.rb2
-rw-r--r--activerecord/CHANGELOG7
-rw-r--r--activerecord/Rakefile2
-rwxr-xr-xactiverecord/lib/active_record/associations.rb151
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb57
-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.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb10
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb13
-rw-r--r--activerecord/lib/active_record/autosave_association.rb240
-rwxr-xr-xactiverecord/lib/active_record/base.rb66
-rw-r--r--activerecord/lib/active_record/batches.rb47
-rw-r--r--activerecord/lib/active_record/calculations.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb64
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb33
-rw-r--r--activerecord/lib/active_record/named_scope.rb30
-rw-r--r--activerecord/lib/active_record/reflection.rb2
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb18
-rw-r--r--activerecord/lib/active_record/session_store.rb10
-rw-r--r--activerecord/lib/active_record/test_case.rb13
-rw-r--r--activerecord/lib/active_record/transactions.rb2
-rw-r--r--activerecord/lib/active_record/validations.rb17
-rw-r--r--activerecord/lib/active_record/version.rb2
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb166
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb29
-rw-r--r--activerecord/test/cases/associations/eager_test.rb8
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb37
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb222
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb29
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb131
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb20
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb2
-rw-r--r--activerecord/test/cases/autosave_association_test.rb509
-rwxr-xr-xactiverecord/test/cases/base_test.rb44
-rw-r--r--activerecord/test/cases/batches_test.rb18
-rw-r--r--activerecord/test/cases/calculations_test.rb25
-rw-r--r--activerecord/test/cases/callbacks_test.rb4
-rw-r--r--activerecord/test/cases/connection_pool_test.rb25
-rw-r--r--activerecord/test/cases/dirty_test.rb2
-rw-r--r--activerecord/test/cases/finder_test.rb79
-rw-r--r--activerecord/test/cases/fixtures_test.rb4
-rw-r--r--activerecord/test/cases/inheritance_test.rb4
-rw-r--r--activerecord/test/cases/locking_test.rb28
-rw-r--r--activerecord/test/cases/method_scoping_test.rb46
-rw-r--r--activerecord/test/cases/migration_test.rb66
-rw-r--r--activerecord/test/cases/named_scope_test.rb55
-rw-r--r--activerecord/test/cases/reflection_test.rb7
-rw-r--r--activerecord/test/cases/transactions_test.rb17
-rw-r--r--activerecord/test/cases/validations_test.rb48
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb13
-rw-r--r--activerecord/test/fixtures/toys.yml4
-rw-r--r--activerecord/test/models/company.rb2
-rw-r--r--activerecord/test/models/event.rb3
-rw-r--r--activerecord/test/models/owner.rb3
-rw-r--r--activerecord/test/models/pet.rb3
-rw-r--r--activerecord/test/models/pirate.rb49
-rw-r--r--activerecord/test/models/post.rb6
-rw-r--r--activerecord/test/models/reply.rb6
-rw-r--r--activerecord/test/models/topic.rb12
-rw-r--r--activerecord/test/models/toy.rb4
-rw-r--r--activerecord/test/schema/schema.rb9
-rw-r--r--activeresource/CHANGELOG4
-rw-r--r--activeresource/README14
-rw-r--r--activeresource/Rakefile2
-rw-r--r--activeresource/lib/active_resource/base.rb20
-rw-r--r--activeresource/lib/active_resource/connection.rb22
-rw-r--r--activeresource/lib/active_resource/validations.rb17
-rw-r--r--activeresource/lib/active_resource/version.rb2
-rw-r--r--activeresource/test/abstract_unit.rb1
-rw-r--r--activeresource/test/authorization_test.rb8
-rw-r--r--activeresource/test/base_test.rb22
-rw-r--r--activesupport/CHANGELOG10
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/hash/indifferent_access.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb4
-rw-r--r--activesupport/lib/active_support/duration.rb2
-rw-r--r--activesupport/lib/active_support/inflector.rb12
-rw-r--r--activesupport/lib/active_support/json/decoding.rb22
-rw-r--r--activesupport/lib/active_support/memoizable.rb4
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb16
-rw-r--r--activesupport/lib/active_support/ordered_hash.rb4
-rw-r--r--activesupport/lib/active_support/test_case.rb2
-rw-r--r--activesupport/lib/active_support/testing/setup_and_teardown.rb7
-rw-r--r--activesupport/lib/active_support/vendor.rb4
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore3
-rwxr-xr-xactivesupport/lib/active_support/vendor/i18n-0.1.3/MIT-LICENSE (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/MIT-LICENSE)0
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.3/README.textile (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/README.textile)0
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.3/Rakefile (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/Rakefile)0
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.3/i18n.gemspec (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec)4
-rwxr-xr-xactivesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n.rb (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb)0
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/backend/simple.rb (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb)10
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/exceptions.rb (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/exceptions.rb)0
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.3/test/all.rb (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/test/all.rb)0
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_exceptions_test.rb (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_exceptions_test.rb)0
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_test.rb (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_test.rb)4
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.rb (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.rb)0
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.yml (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.yml)0
-rw-r--r--activesupport/lib/active_support/vendor/i18n-0.1.3/test/simple_backend_test.rb (renamed from activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb)68
-rw-r--r--activesupport/lib/active_support/version.rb2
-rw-r--r--activesupport/lib/active_support/xml_mini.rb128
-rw-r--r--activesupport/lib/active_support/xml_mini/libxml.rb133
-rw-r--r--activesupport/lib/active_support/xml_mini/nokogiri.rb77
-rw-r--r--activesupport/lib/active_support/xml_mini/rexml.rb108
-rw-r--r--activesupport/test/core_ext/class_test.rb6
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb26
-rw-r--r--activesupport/test/core_ext/load_error_test.rb4
-rw-r--r--activesupport/test/core_ext/module/synchronization_test.rb4
-rw-r--r--activesupport/test/core_ext/module_test.rb10
-rw-r--r--activesupport/test/core_ext/object_and_class_ext_test.rb4
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb18
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb2
-rw-r--r--activesupport/test/dependencies_test.rb22
-rw-r--r--activesupport/test/inflector_test.rb14
-rw-r--r--activesupport/test/inflector_test_cases.rb16
-rw-r--r--activesupport/test/json/decoding_test.rb8
-rw-r--r--activesupport/test/json/encoding_test.rb2
-rw-r--r--activesupport/test/memoizable_test.rb23
-rw-r--r--activesupport/test/message_encryptor_test.rb2
-rw-r--r--activesupport/test/message_verifier_test.rb2
-rw-r--r--activesupport/test/multibyte_chars_test.rb48
-rw-r--r--activesupport/test/ordered_hash_test.rb14
-rw-r--r--activesupport/test/string_inquirer_test.rb2
-rw-r--r--activesupport/test/time_zone_test.rb2
-rw-r--r--activesupport/test/xml_mini/nokogiri_engine_test.rb157
-rw-r--r--activesupport/test/xml_mini/rexml_engine_test.rb15
-rw-r--r--railties/CHANGELOG8
-rw-r--r--railties/Rakefile23
-rw-r--r--railties/builtin/rails_info/rails/info.rb4
-rw-r--r--railties/configs/initializers/backtrace_silencers.rb2
-rw-r--r--railties/environments/boot.rb1
-rw-r--r--railties/guides/files/stylesheets/main.css16
-rw-r--r--railties/guides/images/error_messages.pngbin8440 -> 14645 bytes
-rw-r--r--railties/guides/images/fxn.jpgbin0 -> 17773 bytes
-rw-r--r--railties/guides/rails_guides.rb28
-rw-r--r--railties/guides/rails_guides/generator.rb28
-rw-r--r--railties/guides/rails_guides/indexer.rb2
-rw-r--r--railties/guides/rails_guides/levenshtein.rb29
-rw-r--r--railties/guides/source/2_3_release_notes.textile115
-rw-r--r--railties/guides/source/action_controller_overview.textile32
-rw-r--r--railties/guides/source/action_mailer_basics.textile60
-rw-r--r--railties/guides/source/active_record_basics.textile8
-rw-r--r--railties/guides/source/active_record_querying.textile171
-rw-r--r--railties/guides/source/activerecord_validations_callbacks.textile543
-rw-r--r--railties/guides/source/association_basics.textile80
-rw-r--r--railties/guides/source/caching_with_rails.textile214
-rw-r--r--railties/guides/source/command_line.textile43
-rw-r--r--railties/guides/source/contribute.textile22
-rw-r--r--railties/guides/source/contributing_to_rails.textile (renamed from railties/guides/source/contributing.textile)27
-rw-r--r--railties/guides/source/credits.erb.textile33
-rw-r--r--railties/guides/source/debugging_rails_applications.textile12
-rw-r--r--railties/guides/source/form_helpers.textile54
-rw-r--r--railties/guides/source/getting_started.textile16
-rw-r--r--railties/guides/source/i18n.textile268
-rw-r--r--railties/guides/source/index.erb.textile12
-rw-r--r--railties/guides/source/layout.html.erb20
-rw-r--r--railties/guides/source/layouts_and_rendering.textile64
-rw-r--r--railties/guides/source/migrations.textile46
-rw-r--r--railties/guides/source/nested_model_forms.textile222
-rw-r--r--railties/guides/source/performance_testing.textile56
-rw-r--r--railties/guides/source/plugins.textile34
-rw-r--r--railties/guides/source/rails_on_rack.textile97
-rw-r--r--railties/guides/source/routing.textile40
-rw-r--r--railties/guides/source/security.textile72
-rw-r--r--railties/guides/source/testing.textile30
-rw-r--r--railties/lib/commands/plugin.rb453
-rw-r--r--railties/lib/console_app.rb1
-rw-r--r--railties/lib/initializer.rb28
-rw-r--r--railties/lib/rails/backtrace_cleaner.rb23
-rw-r--r--railties/lib/rails/gem_dependency.rb218
-rw-r--r--railties/lib/rails/plugin.rb8
-rw-r--r--railties/lib/rails/plugin/loader.rb7
-rw-r--r--railties/lib/rails/rack/metal.rb27
-rw-r--r--railties/lib/rails/rack/static.rb17
-rw-r--r--railties/lib/rails/version.rb2
-rw-r--r--railties/lib/rails_generator/commands.rb8
-rw-r--r--railties/lib/rails_generator/generators/applications/app/template_runner.rb32
-rw-r--r--railties/lib/tasks/gems.rake79
-rw-r--r--railties/lib/tasks/testing.rake2
-rw-r--r--railties/test/abstract_unit.rb2
-rw-r--r--railties/test/backtrace_cleaner_test.rb65
-rw-r--r--railties/test/boot_test.rb2
-rw-r--r--railties/test/console_app_test.rb42
-rw-r--r--railties/test/fixtures/metal/multiplemetals/app/metal/metal_a.rb5
-rw-r--r--railties/test/fixtures/metal/multiplemetals/app/metal/metal_b.rb5
-rw-r--r--railties/test/fixtures/metal/pluralmetal/app/metal/legacy_routes.rb5
-rw-r--r--railties/test/fixtures/metal/singlemetal/app/metal/foo_metal.rb5
-rw-r--r--railties/test/fixtures/metal/subfolders/app/metal/Folder/metal_a.rb7
-rw-r--r--railties/test/fixtures/metal/subfolders/app/metal/Folder/metal_b.rb7
-rw-r--r--railties/test/fixtures/plugins/engines/engine/app/metal/engine_metal.rb10
-rw-r--r--railties/test/fixtures/plugins/engines/engine/init.rb2
-rw-r--r--railties/test/fixtures/public/foo/bar.html1
-rw-r--r--railties/test/fixtures/public/foo/index.html1
-rw-r--r--railties/test/fixtures/public/index.html1
-rw-r--r--railties/test/gem_dependency_test.rb17
-rw-r--r--railties/test/generators/rails_template_runner_test.rb16
-rw-r--r--railties/test/initializer_test.rb83
-rw-r--r--railties/test/metal_test.rb72
-rw-r--r--railties/test/plugin_loader_test.rb4
-rw-r--r--railties/test/plugin_locator_test.rb2
-rw-r--r--railties/test/plugin_test.rb12
-rw-r--r--railties/test/rack_static_test.rb46
-rw-r--r--railties/test/vendor/gems/dummy-gem-g-1.0.0/.specification2
311 files changed, 6219 insertions, 3217 deletions
diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG
index 457e9aca94..773e603d73 100644
--- a/actionmailer/CHANGELOG
+++ b/actionmailer/CHANGELOG
@@ -1,10 +1,7 @@
-*Edge*
+*2.3.2 [Final] (March 15, 2009)*
* Fixed that ActionMailer should send correctly formatted Return-Path in MAIL FROM for SMTP #1842 [Matt Jones]
-
-*2.3.0 [RC1] (February 1st, 2009)*
-
* Fixed RFC-2045 quoted-printable bug #1421 [squadette]
* Fixed that no body charset would be set when there are attachments present #740 [Paweł Kondzior]
diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile
index c3826e3a27..f06f18ff76 100644
--- a/actionmailer/Rakefile
+++ b/actionmailer/Rakefile
@@ -55,7 +55,7 @@ spec = Gem::Specification.new do |s|
s.rubyforge_project = "actionmailer"
s.homepage = "http://www.rubyonrails.org"
- s.add_dependency('actionpack', '= 2.3.0' + PKG_BUILD)
+ s.add_dependency('actionpack', '= 2.3.2' + PKG_BUILD)
s.has_rdoc = true
s.requirements << 'none'
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
index 45fcab599c..02c536c8ad 100644
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -58,4 +58,5 @@ module Net
end
autoload :MailHelper, 'action_mailer/mail_helper'
-autoload :TMail, 'action_mailer/vendor/tmail'
+
+require 'action_mailer/vendor/tmail'
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index db4589ee8f..b77409b64b 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -479,7 +479,7 @@ module ActionMailer #:nodoc:
)
end
unless @parts.empty?
- @content_type = "multipart/alternative"
+ @content_type = "multipart/alternative" if @content_type !~ /^multipart/
@parts = sort_parts(@parts, @implicit_parts_order)
end
end
diff --git a/actionmailer/lib/action_mailer/part.rb b/actionmailer/lib/action_mailer/part.rb
index 977c0b2b5c..2bbb59cdb6 100644
--- a/actionmailer/lib/action_mailer/part.rb
+++ b/actionmailer/lib/action_mailer/part.rb
@@ -88,7 +88,10 @@ module ActionMailer
part.parts << prt
end
- part.set_content_type(real_content_type, nil, ctype_attrs) if real_content_type =~ /multipart/
+ if real_content_type =~ /multipart/
+ ctype_attrs.delete 'charset'
+ part.set_content_type(real_content_type, nil, ctype_attrs)
+ end
end
headers.each { |k,v| part[k] = v }
diff --git a/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb b/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb
index de054db83e..2d20c7a6a1 100755
--- a/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb
+++ b/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb
@@ -1150,7 +1150,7 @@ if __FILE__ == $0
assert_equal(Text::Format::JUSTIFY, @format_o.format_style)
assert_match(/^of freedom, and that government of the people, by the people, for the$/,
@format_o.format(GETTYSBURG).split("\n")[-3])
- assert_raises(ArgumentError) { @format_o.format_style = 33 }
+ assert_raise(ArgumentError) { @format_o.format_style = 33 }
end
def test_tag_paragraph
diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb
index 9cd7a14b73..08ff0d2ffb 100644
--- a/actionmailer/lib/action_mailer/version.rb
+++ b/actionmailer/lib/action_mailer/version.rb
@@ -2,7 +2,7 @@ module ActionMailer
module VERSION #:nodoc:
MAJOR = 2
MINOR = 3
- TINY = 0
+ TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end
diff --git a/actionmailer/test/mail_layout_test.rb b/actionmailer/test/mail_layout_test.rb
index c185bd5acd..50901f52ec 100644
--- a/actionmailer/test/mail_layout_test.rb
+++ b/actionmailer/test/mail_layout_test.rb
@@ -21,10 +21,12 @@ class AutoLayoutMailer < ActionMailer::Base
body render(:inline => "Hello, <%= @world %>", :layout => false, :body => { :world => "Earth" })
end
- def multipart(recipient)
+ def multipart(recipient, type = nil)
recipients recipient
subject "You have a mail"
from "tester@example.com"
+
+ content_type(type) if type
end
end
@@ -64,6 +66,19 @@ class LayoutMailerTest < Test::Unit::TestCase
def test_should_pickup_multipart_layout
mail = AutoLayoutMailer.create_multipart(@recipient)
+ assert_equal "multipart/alternative", mail.content_type
+ assert_equal 2, mail.parts.size
+
+ assert_equal 'text/plain', mail.parts.first.content_type
+ assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
+
+ assert_equal 'text/html', mail.parts.last.content_type
+ assert_equal "Hello from layout text/html multipart", mail.parts.last.body
+ end
+
+ def test_should_pickup_multipartmixed_layout
+ mail = AutoLayoutMailer.create_multipart(@recipient, "multipart/mixed")
+ assert_equal "multipart/mixed", mail.content_type
assert_equal 2, mail.parts.size
assert_equal 'text/plain', mail.parts.first.content_type
@@ -73,6 +88,19 @@ class LayoutMailerTest < Test::Unit::TestCase
assert_equal "Hello from layout text/html multipart", mail.parts.last.body
end
+ def test_should_fix_multipart_layout
+ mail = AutoLayoutMailer.create_multipart(@recipient, "text/plain")
+ assert_equal "multipart/alternative", mail.content_type
+ assert_equal 2, mail.parts.size
+
+ assert_equal 'text/plain', mail.parts.first.content_type
+ assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
+
+ assert_equal 'text/html', mail.parts.last.content_type
+ assert_equal "Hello from layout text/html multipart", mail.parts.last.body
+ end
+
+
def test_should_pickup_layout_given_to_render
mail = AutoLayoutMailer.create_spam(@recipient)
assert_equal "Spammer layout Hello, Earth", mail.body.strip
diff --git a/actionmailer/test/mail_service_test.rb b/actionmailer/test/mail_service_test.rb
index 1e04531753..277a91395d 100644
--- a/actionmailer/test/mail_service_test.rb
+++ b/actionmailer/test/mail_service_test.rb
@@ -330,6 +330,7 @@ class ActionMailerTest < Test::Unit::TestCase
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
+ assert_nil created.parts.first.charset
assert_equal "text/plain", created.parts.first.parts.first.content_type
assert_equal "text/html", created.parts.first.parts[1].content_type
assert_equal "application/octet-stream", created.parts[1].content_type
@@ -1068,7 +1069,7 @@ class RespondToTest < Test::Unit::TestCase
end
def test_should_still_raise_exception_with_expected_message_when_calling_an_undefined_method
- error = assert_raises NoMethodError do
+ error = assert_raise NoMethodError do
RespondToMailer.not_a_method
end
diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb
index 9d22bb26bd..65b07a71b8 100644
--- a/actionmailer/test/test_helper_test.rb
+++ b/actionmailer/test/test_helper_test.rb
@@ -26,7 +26,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
def test_determine_default_mailer_raises_correct_error
- assert_raises(ActionMailer::NonInferrableMailerError) do
+ assert_raise(ActionMailer::NonInferrableMailerError) do
self.class.determine_default_mailer("NotAMailerTest")
end
end
@@ -84,7 +84,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
def test_assert_emails_too_few_sent
- error = assert_raises ActiveSupport::TestCase::Assertion do
+ error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 2 do
TestHelperMailer.deliver_test
end
@@ -94,7 +94,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
def test_assert_emails_too_many_sent
- error = assert_raises ActiveSupport::TestCase::Assertion do
+ error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 1 do
TestHelperMailer.deliver_test
TestHelperMailer.deliver_test
@@ -105,7 +105,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
def test_assert_no_emails_failure
- error = assert_raises ActiveSupport::TestCase::Assertion do
+ error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_emails do
TestHelperMailer.deliver_test
end
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
index 546adeb61d..8c9486cc63 100644
--- a/actionpack/CHANGELOG
+++ b/actionpack/CHANGELOG
@@ -1,4 +1,12 @@
-*Edge*
+*2.3.2 [Final] (March 15, 2009)*
+
+* Fixed that redirection would just log the options, not the final url (which lead to "Redirected to #<Post:0x23150b8>") [DHH]
+
+* Added ability to pass in :public => true to fresh_when, stale?, and expires_in to make the request proxy cachable #2095 [Gregg Pollack]
+
+* Fixed that passing a custom form builder would be forwarded to nested fields_for calls #2023 [Eloy Duran/Nate Wiger]
+
+* Form option helpers now support disabled option tags and the use of lambdas for selecting/disabling option tags from collections #837 [Tekin]
* Added partial scoping to TranslationHelper#translate, so if you call translate(".foo") from the people/index.html.erb template, you'll actually be calling I18n.translate("people.index.foo") [DHH]
@@ -6,9 +14,6 @@
* Added localized rescue template when I18n.locale is set (ex: public/404.da.html) #1835 [José Valim]
-
-*2.3.0 [RC1] (February 1st, 2009)*
-
* Make the form_for and fields_for helpers support the new Active Record nested update options. #1202 [Eloy Duran]
<% form_for @person do |person_form| %>
diff --git a/actionpack/Rakefile b/actionpack/Rakefile
index c389e5a8d6..6cacdf3c6e 100644
--- a/actionpack/Rakefile
+++ b/actionpack/Rakefile
@@ -80,8 +80,7 @@ spec = Gem::Specification.new do |s|
s.has_rdoc = true
s.requirements << 'none'
- s.add_dependency('activesupport', '= 2.3.0' + PKG_BUILD)
- s.add_dependency('rack', '>= 0.9.0')
+ s.add_dependency('activesupport', '= 2.3.2' + PKG_BUILD)
s.require_path = 'lib'
s.autorequire = 'action_controller'
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index ca826e7bfc..d03f4cb231 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -31,7 +31,12 @@ rescue LoadError
end
end
-require 'action_controller/vendor/rack-1.0/rack'
+begin
+ gem 'rack', '~> 1.0.0'
+ require 'rack'
+rescue Gem::LoadError
+ require 'action_controller/vendor/rack-1.0/rack'
+end
module ActionController
# TODO: Review explicit to see if they will automatically be handled by
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 4fc52c1470..6348961ff4 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -22,7 +22,7 @@ module ActionController #:nodoc:
attr_reader :allowed_methods
def initialize(*allowed_methods)
- super("Only #{allowed_methods.to_sentence} requests are allowed.")
+ super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.")
@allowed_methods = allowed_methods
end
@@ -408,7 +408,7 @@ module ActionController #:nodoc:
# Return an array containing the names of public methods that have been marked hidden from the action processor.
# By default, all methods defined in ActionController::Base and included modules are hidden.
- # More methods can be hidden using <tt>hide_actions</tt>.
+ # More methods can be hidden using <tt>hide_action</tt>.
def hidden_actions
read_inheritable_attribute(:hidden_actions) || write_inheritable_attribute(:hidden_actions, [])
end
@@ -908,12 +908,14 @@ module ActionController #:nodoc:
end
options = extra_options
+ elsif !options.is_a?(Hash)
+ extra_options[:partial] = options
+ options = extra_options
end
layout = pick_layout(options)
response.layout = layout.path_without_format_and_extension if layout
logger.info("Rendering template within #{layout.path_without_format_and_extension}") if logger && layout
- layout = layout.path_without_format_and_extension if layout
if content_type = options[:content_type]
response.content_type = content_type.to_s
@@ -982,6 +984,7 @@ module ActionController #:nodoc:
# of sending it as the response body to the browser.
def render_to_string(options = nil, &block) #:doc:
render(options, &block)
+ response.body
ensure
response.content_type = nil
erase_render_results
@@ -1018,7 +1021,7 @@ module ActionController #:nodoc:
# Clears the rendered results, allowing for another render to be performed.
def erase_render_results #:nodoc:
- response.body = nil
+ response.body = []
@performed_render = false
end
@@ -1101,7 +1104,6 @@ module ActionController #:nodoc:
end
response.redirected_to = options
- logger.info("Redirected to #{options}") if logger && logger.info?
case options
# The scheme name consist of a letter followed by any combination of
@@ -1124,6 +1126,7 @@ module ActionController #:nodoc:
def redirect_to_full_url(url, status)
raise DoubleRenderError if performed?
+ logger.info("Redirected to #{url}") if logger && logger.info?
response.redirect(url, interpret_status(status))
@performed_redirect = true
end
@@ -1133,6 +1136,11 @@ module ActionController #:nodoc:
# request is considered stale and should be generated from scratch. Otherwise,
# it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent.
#
+ # Parameters:
+ # * <tt>:etag</tt>
+ # * <tt>:last_modified</tt>
+ # * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches).
+ #
# Example:
#
# def show
@@ -1153,20 +1161,34 @@ module ActionController #:nodoc:
# Sets the etag, last_modified, or both on the response and renders a
# "304 Not Modified" response if the request is already fresh.
#
+ # Parameters:
+ # * <tt>:etag</tt>
+ # * <tt>:last_modified</tt>
+ # * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches).
+ #
# Example:
#
# def show
# @article = Article.find(params[:id])
- # fresh_when(:etag => @article, :last_modified => @article.created_at.utc)
+ # fresh_when(:etag => @article, :last_modified => @article.created_at.utc, :public => true)
# end
#
# This will render the show template if the request isn't sending a matching etag or
# If-Modified-Since header and just a "304 Not Modified" response if there's a match.
+ #
def fresh_when(options)
- options.assert_valid_keys(:etag, :last_modified)
+ options.assert_valid_keys(:etag, :last_modified, :public)
response.etag = options[:etag] if options[:etag]
response.last_modified = options[:last_modified] if options[:last_modified]
+
+ if options[:public]
+ cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip }
+ cache_control.delete("private")
+ cache_control.delete("no-cache")
+ cache_control << "public"
+ response.headers["Cache-Control"] = cache_control.join(', ')
+ end
if request.fresh?(response)
head :not_modified
@@ -1178,15 +1200,26 @@ module ActionController #:nodoc:
#
# Examples:
# expires_in 20.minutes
- # expires_in 3.hours, :private => false
- # expires in 3.hours, 'max-stale' => 5.hours, :private => nil, :public => true
+ # expires_in 3.hours, :public => true
+ # expires in 3.hours, 'max-stale' => 5.hours, :public => true
#
# This method will overwrite an existing Cache-Control header.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
def expires_in(seconds, options = {}) #:doc:
- cache_options = { 'max-age' => seconds, 'private' => true }.symbolize_keys.merge!(options.symbolize_keys)
- cache_options.delete_if { |k,v| v.nil? or v == false }
- cache_control = cache_options.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"}
+ cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip }
+
+ cache_control << "max-age=#{seconds}"
+ cache_control.delete("no-cache")
+ if options[:public]
+ cache_control.delete("private")
+ cache_control << "public"
+ else
+ cache_control << "private"
+ end
+
+ # This allows for additional headers to be passed through like 'max-stale' => 5.hours
+ cache_control += options.symbolize_keys.reject{|k,v| k == :public || k == :private }.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"}
+
response.headers["Cache-Control"] = cache_control.join(', ')
end
@@ -1215,13 +1248,12 @@ module ActionController #:nodoc:
response.status = interpret_status(status || DEFAULT_RENDER_STATUS_CODE)
if append_response
- response.body ||= ''
- response.body << text.to_s
+ response.body_parts << text.to_s
else
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
+ 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
@@ -1298,7 +1330,7 @@ module ActionController #:nodoc:
rescue ActionView::MissingTemplate => e
# Was the implicit template missing, or was it another template?
if e.path == default_template_name
- raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller
+ raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence(:locale => :en)}", caller
else
raise e
end
diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb
index 34e1c3527f..87b5029e57 100644
--- a/actionpack/lib/action_controller/caching/actions.rb
+++ b/actionpack/lib/action_controller/caching/actions.rb
@@ -129,24 +129,23 @@ module ActionController #:nodoc:
attr_reader :path, :extension
class << self
- def path_for(controller, options, infer_extension=true)
+ def path_for(controller, options, infer_extension = true)
new(controller, options, infer_extension).path
end
end
# When true, infer_extension will look up the cache path extension from the request's path & format.
- # This is desirable when reading and writing the cache, but not when expiring the cache - expire_action should expire the same files regardless of the request format.
- def initialize(controller, options = {}, infer_extension=true)
- if infer_extension and options.is_a? Hash
- request_extension = extract_extension(controller.request)
- options = options.reverse_merge(:format => request_extension)
+ # This is desirable when reading and writing the cache, but not when expiring the cache -
+ # expire_action should expire the same files regardless of the request format.
+ def initialize(controller, options = {}, infer_extension = true)
+ if infer_extension
+ extract_extension(controller.request)
+ options = options.reverse_merge(:format => @extension) if options.is_a?(Hash)
end
+
path = controller.url_for(options).split('://').last
normalize!(path)
- if infer_extension
- @extension = request_extension
- add_extension!(path, @extension)
- end
+ add_extension!(path, @extension)
@path = URI.unescape(path)
end
@@ -162,13 +161,7 @@ module ActionController #:nodoc:
def extract_extension(request)
# Don't want just what comes after the last '.' to accommodate multi part extensions
# such as tar.gz.
- extension = request.path[/^[^.]+\.(.+)$/, 1]
-
- # If there's no extension in the path, check request.format
- if extension.nil?
- extension = request.cache_format
- end
- extension
+ @extension = request.path[/^[^.]+\.(.+)$/, 1] || request.cache_format
end
end
end
diff --git a/actionpack/lib/action_controller/dispatcher.rb b/actionpack/lib/action_controller/dispatcher.rb
index ec40b5c4e6..07931e4a4a 100644
--- a/actionpack/lib/action_controller/dispatcher.rb
+++ b/actionpack/lib/action_controller/dispatcher.rb
@@ -13,7 +13,6 @@ module ActionController
end
if defined?(ActiveRecord)
- after_dispatch :checkin_connections
to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers }
end
@@ -115,11 +114,5 @@ module ActionController
def flush_logger
Base.logger.flush
end
-
- def checkin_connections
- # Don't return connection (and peform implicit rollback) if this request is a part of integration test
- return if @env.key?("rack.test")
- ActiveRecord::Base.clear_active_connections!
- end
end
end
diff --git a/actionpack/lib/action_controller/http_authentication.rb b/actionpack/lib/action_controller/http_authentication.rb
index 2ccbc22420..b6b5267c66 100644
--- a/actionpack/lib/action_controller/http_authentication.rb
+++ b/actionpack/lib/action_controller/http_authentication.rb
@@ -68,8 +68,11 @@ module ActionController
#
# Simple Digest example:
#
+ # require 'digest/md5'
# class PostsController < ApplicationController
- # USERS = {"dhh" => "secret"}
+ # REALM = "SuperSecret"
+ # USERS = {"dhh" => "secret", #plain text password
+ # "dap" => Digest:MD5::hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password
#
# before_filter :authenticate, :except => [:index]
#
@@ -83,14 +86,18 @@ module ActionController
#
# private
# def authenticate
- # authenticate_or_request_with_http_digest(realm) do |username|
+ # authenticate_or_request_with_http_digest(REALM) do |username|
# USERS[username]
# end
# end
# end
#
- # NOTE: The +authenticate_or_request_with_http_digest+ block must return the user's password so the framework can appropriately
- # hash it to check the user's credentials. Returning +nil+ will cause authentication to fail.
+ # NOTE: The +authenticate_or_request_with_http_digest+ block must return the user's password or the ha1 digest hash so the framework can appropriately
+ # hash to check the user's credentials. Returning +nil+ will cause authentication to fail.
+ # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
+ # the password file or database is compromised, the attacker would be able to use the ha1 hash to
+ # authenticate as the user at this +realm+, but would not have the user's password to try using at
+ # other sites.
#
# On shared hosts, Apache sometimes doesn't pass authentication headers to
# FCGI instances. If your environment matches this description and you cannot
@@ -177,26 +184,37 @@ module ActionController
end
# Raises error unless the request credentials response value matches the expected value.
+ # First try the password as a ha1 digest password. If this fails, then try it as a plain
+ # text password.
def validate_digest_response(request, realm, &password_procedure)
credentials = decode_credentials_header(request)
valid_nonce = validate_nonce(request, credentials[:nonce])
- if valid_nonce && realm == credentials[:realm] && opaque(request.session.session_id) == credentials[:opaque]
+ if valid_nonce && realm == credentials[:realm] && opaque == credentials[:opaque]
password = password_procedure.call(credentials[:username])
- expected = expected_response(request.env['REQUEST_METHOD'], credentials[:uri], credentials, password)
- expected == credentials[:response]
+
+ [true, false].any? do |password_is_ha1|
+ expected = expected_response(request.env['REQUEST_METHOD'], request.env['REQUEST_URI'], credentials, password, password_is_ha1)
+ expected == credentials[:response]
+ end
end
end
# Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
- def expected_response(http_method, uri, credentials, password)
- ha1 = ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
+ # Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
+ # of a plain-text password.
+ def expected_response(http_method, uri, credentials, password, password_is_ha1=true)
+ ha1 = password_is_ha1 ? password : ha1(credentials, password)
ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(':'))
::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(':'))
end
- def encode_credentials(http_method, credentials, password)
- credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password)
+ def ha1(credentials, password)
+ ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
+ end
+
+ def encode_credentials(http_method, credentials, password, password_is_ha1)
+ credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
"Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ')
end
@@ -213,8 +231,7 @@ module ActionController
end
def authentication_header(controller, realm)
- session_id = controller.request.session.session_id
- controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce(session_id)}", opaque="#{opaque(session_id)}")
+ controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
end
def authentication_request(controller, realm, message = nil)
@@ -252,23 +269,36 @@ module ActionController
# POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
# of this document.
#
- # The nonce is opaque to the client.
- def nonce(session_id, time = Time.now)
+ # The nonce is opaque to the client. Composed of Time, and hash of Time with secret
+ # key from the Rails session secret generated upon creation of project. Ensures
+ # the time cannot be modifed by client.
+ def nonce(time = Time.now)
t = time.to_i
- hashed = [t, session_id]
+ hashed = [t, secret_key]
digest = ::Digest::MD5.hexdigest(hashed.join(":"))
Base64.encode64("#{t}:#{digest}").gsub("\n", '')
end
- def validate_nonce(request, value)
+ # Might want a shorter timeout depending on whether the request
+ # is a PUT or POST, and if client is browser or web service.
+ # Can be much shorter if the Stale directive is implemented. This would
+ # allow a user to use new nonce without prompting user again for their
+ # username and password.
+ def validate_nonce(request, value, seconds_to_timeout=5*60)
t = Base64.decode64(value).split(":").first.to_i
- nonce(request.session.session_id, t) == value && (t - Time.now.to_i).abs <= 10 * 60
+ nonce(t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
end
- # Opaque based on digest of session_id
- def opaque(session_id)
- Base64.encode64(::Digest::MD5::hexdigest(session_id)).gsub("\n", '')
+ # Opaque based on random generation - but changing each request?
+ def opaque()
+ ::Digest::MD5.hexdigest(secret_key)
end
+
+ # Set in /initializers/session_store.rb, and loaded even if sessions are not in use.
+ def secret_key
+ ActionController::Base.session_options[:secret]
+ end
+
end
end
end
diff --git a/actionpack/lib/action_controller/integration.rb b/actionpack/lib/action_controller/integration.rb
index 1c05ab0bf6..fda6b639d1 100644
--- a/actionpack/lib/action_controller/integration.rb
+++ b/actionpack/lib/action_controller/integration.rb
@@ -5,7 +5,7 @@ require 'active_support/test_case'
module ActionController
module Integration #:nodoc:
# An integration Session instance represents a set of requests and responses
- # performed sequentially by some virtual user. Becase you can instantiate
+ # performed sequentially by some virtual user. Because you can instantiate
# multiple sessions and run them side-by-side, you can also mimic (to some
# limited extent) multiple simultaneous users interacting with your system.
#
@@ -332,11 +332,13 @@ module ActionController
@cookies[name] = value
end
- @body = ""
if body.is_a?(String)
- @body << body
+ @body_parts = [body]
+ @body = body
else
- body.each { |part| @body << part }
+ @body_parts = []
+ body.each { |part| @body_parts << part.to_s }
+ @body = @body_parts.join
end
if @controller = ActionController::Base.last_instantiation
@@ -349,7 +351,7 @@ module ActionController
@response = Response.new
@response.status = status.to_s
@response.headers.replace(@headers)
- @response.body = @body
+ @response.body = @body_parts
end
# Decorate the response with the standard behavior of the
diff --git a/actionpack/lib/action_controller/layout.rb b/actionpack/lib/action_controller/layout.rb
index a0db7acf72..6ec0c1b304 100644
--- a/actionpack/lib/action_controller/layout.rb
+++ b/actionpack/lib/action_controller/layout.rb
@@ -198,7 +198,7 @@ module ActionController #:nodoc:
# is called and the return value is used. Likewise if the layout was specified as an inline method (through a proc or method
# object). If the layout was defined without a directory, layouts is assumed. So <tt>layout "weblog/standard"</tt> will return
# weblog/standard, but <tt>layout "standard"</tt> will return layouts/standard.
- def active_layout(passed_layout = nil)
+ def active_layout(passed_layout = nil, options = {})
layout = passed_layout || default_layout
return layout if layout.respond_to?(:render)
@@ -207,21 +207,23 @@ module ActionController #:nodoc:
when Proc then layout.call(self)
else layout
end
-
- find_layout(active_layout, @template.template_format) if active_layout
+
+ find_layout(active_layout, default_template_format, options[:html_fallback]) if active_layout
end
private
def default_layout #:nodoc:
- layout = self.class.read_inheritable_attribute(:layout) unless default_template_format == :js
+ layout = self.class.read_inheritable_attribute(:layout)
return layout unless self.class.read_inheritable_attribute(:auto_layout)
find_layout(layout, default_template_format)
rescue ActionView::MissingTemplate
nil
end
- def find_layout(layout, *formats) #:nodoc:
- view_paths.find_template(layout.to_s =~ /layouts\// ? layout : "layouts/#{layout}", *formats)
+ def find_layout(layout, format, html_fallback=false) #:nodoc:
+ view_paths.find_template(layout.to_s =~ /layouts\// ? layout : "layouts/#{layout}", format, html_fallback)
+ rescue ActionView::MissingTemplate
+ raise if Mime::Type.lookup_by_extension(format.to_s).html?
end
def pick_layout(options)
@@ -232,7 +234,7 @@ module ActionController #:nodoc:
when NilClass, TrueClass
active_layout if action_has_layout? && candidate_for_layout?(:template => default_template_name)
else
- active_layout(layout)
+ active_layout(layout, :html_fallback => true)
end
else
active_layout if action_has_layout? && candidate_for_layout?(options)
@@ -258,7 +260,12 @@ module ActionController #:nodoc:
template = options[:template] || default_template(options[:action])
if options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty?
begin
- !self.view_paths.find_template(template, default_template_format).exempt_from_layout?
+ template_object = self.view_paths.find_template(template, default_template_format)
+ # this restores the behavior from 2.2.2, where response.template.template_format was reset
+ # to :html for :js requests with a matching html template.
+ # see v2.2.2, ActionView::Base, lines 328-330
+ @real_format = :html if response.template.template_format == :js && template_object.format == "html"
+ !template_object.exempt_from_layout?
rescue ActionView::MissingTemplate
true
end
@@ -268,7 +275,7 @@ module ActionController #:nodoc:
end
def default_template_format
- response.template.template_format
+ @real_format || response.template.template_format
end
end
end
diff --git a/actionpack/lib/action_controller/polymorphic_routes.rb b/actionpack/lib/action_controller/polymorphic_routes.rb
index 924d1aa6bd..d9b614c237 100644
--- a/actionpack/lib/action_controller/polymorphic_routes.rb
+++ b/actionpack/lib/action_controller/polymorphic_routes.rb
@@ -163,7 +163,8 @@ module ActionController
if parent.is_a?(Symbol) || parent.is_a?(String)
string << "#{parent}_"
else
- string << "#{RecordIdentifier.__send__("singular_class_name", parent)}_"
+ string << "#{RecordIdentifier.__send__("plural_class_name", parent)}".singularize
+ string << "_"
end
end
end
@@ -171,7 +172,9 @@ module ActionController
if record.is_a?(Symbol) || record.is_a?(String)
route << "#{record}_"
else
- route << "#{RecordIdentifier.__send__("#{inflection}_class_name", record)}_"
+ route << "#{RecordIdentifier.__send__("plural_class_name", record)}"
+ route = route.singularize if inflection == :singular
+ route << "_"
end
action_prefix(options) + namespace + route + routing_type(options).to_s
diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb
index 0e95cfc147..ef223f157c 100755
--- a/actionpack/lib/action_controller/request.rb
+++ b/actionpack/lib/action_controller/request.rb
@@ -32,7 +32,7 @@ module ActionController
# <tt>:get</tt>. If the request \method is not listed in the HTTP_METHODS
# constant above, an UnknownHttpMethod exception is raised.
def request_method
- @request_method ||= HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
+ @request_method ||= HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
end
# Returns the HTTP request \method used for action processing as a
@@ -442,6 +442,7 @@ EOM
end
def reset_session
+ @env['rack.session.options'].delete(:id)
@env['rack.session'] = {}
end
diff --git a/actionpack/lib/action_controller/resources.rb b/actionpack/lib/action_controller/resources.rb
index 3af21967df..86abb7b2f4 100644
--- a/actionpack/lib/action_controller/resources.rb
+++ b/actionpack/lib/action_controller/resources.rb
@@ -91,7 +91,7 @@ module ActionController
end
def shallow_path_prefix
- @shallow_path_prefix ||= "#{path_prefix unless @options[:shallow]}"
+ @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix
end
def member_path
@@ -103,7 +103,7 @@ module ActionController
end
def shallow_name_prefix
- @shallow_name_prefix ||= "#{name_prefix unless @options[:shallow]}"
+ @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix
end
def nesting_name_prefix
@@ -630,7 +630,7 @@ 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_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m)
+ map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true })
end
end
end
@@ -641,9 +641,9 @@ module ActionController
map_resource_routes(map, resource, :destroy, resource.member_path, route_path)
end
- def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil)
+ def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} )
if resource.has_action?(action)
- action_options = action_options_for(action, resource, method)
+ action_options = action_options_for(action, resource, method, resource_options)
formatted_route_path = "#{route_path}.:format"
if route_name && @set.named_routes[route_name.to_sym].nil?
@@ -660,9 +660,10 @@ module ActionController
end
end
- def action_options_for(action, resource, method = nil)
+ def action_options_for(action, resource, method = nil, resource_options = {})
default_options = { :action => action.to_s }
require_id = !resource.kind_of?(SingletonResource)
+ force_id = resource_options[:force_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)
@@ -670,7 +671,7 @@ module ActionController
when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id))
when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id))
when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id))
- else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements)
+ else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id))
end
end
end
diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb
index ccff473df0..febe4ccf29 100644
--- a/actionpack/lib/action_controller/response.rb
+++ b/actionpack/lib/action_controller/response.rb
@@ -40,14 +40,28 @@ module ActionController # :nodoc:
delegate :default_charset, :to => 'ActionController::Base'
def initialize
- @status = 200
+ super
@header = Rack::Utils::HeaderHash.new(DEFAULT_HEADERS)
+ @session, @assigns = [], []
+ end
- @writer = lambda { |x| @body << x }
- @block = nil
+ def body
+ str = ''
+ each { |part| str << part.to_s }
+ str
+ end
- @body = "",
- @session, @assigns = [], []
+ def body=(body)
+ @body =
+ if body.is_a?(String)
+ [body]
+ else
+ body
+ end
+ end
+
+ def body_parts
+ @body
end
def location; headers['Location'] end
@@ -152,7 +166,7 @@ module ActionController # :nodoc:
@writer = lambda { |x| callback.call(x) }
@body.call(self, self)
elsif @body.is_a?(String)
- @body.each_line(&callback)
+ callback.call(@body)
else
@body.each(&callback)
end
@@ -162,7 +176,8 @@ module ActionController # :nodoc:
end
def write(str)
- @writer.call str.to_s
+ str = str.to_s
+ @writer.call str
str
end
@@ -186,7 +201,7 @@ module ActionController # :nodoc:
if request && request.etag_matches?(etag)
self.status = '304 Not Modified'
- self.body = ''
+ self.body = []
end
set_conditional_cache_control!
@@ -195,7 +210,11 @@ module ActionController # :nodoc:
def nonempty_ok_response?
ok = !status || status.to_s[0..2] == '200'
- ok && body.is_a?(String) && !body.empty?
+ ok && string_body?
+ end
+
+ def string_body?
+ !body_parts.respond_to?(:call) && body_parts.any? && body_parts.all? { |part| part.is_a?(String) }
end
def set_conditional_cache_control!
@@ -216,8 +235,8 @@ module ActionController # :nodoc:
headers.delete('Content-Length')
elsif length = headers['Content-Length']
headers['Content-Length'] = length.to_s
- elsif !body.respond_to?(:call) && (!status || status.to_s[0..2] != '304')
- headers["Content-Length"] = (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s
+ elsif string_body? && (!status || status.to_s[0..2] != '304')
+ headers["Content-Length"] = Rack::Utils.bytesize(body).to_s
end
end
diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb
index a2141a77dc..c0eb61340b 100644
--- a/actionpack/lib/action_controller/routing.rb
+++ b/actionpack/lib/action_controller/routing.rb
@@ -267,7 +267,7 @@ module ActionController
module Routing
SEPARATORS = %w( / . ? )
- HTTP_METHODS = [:get, :head, :post, :put, :delete]
+ HTTP_METHODS = [:get, :head, :post, :put, :delete, :options]
ALLOWED_REQUIREMENTS_FOR_OPTIMISATION = [:controller, :action].to_set
diff --git a/actionpack/lib/action_controller/routing/builder.rb b/actionpack/lib/action_controller/routing/builder.rb
index 44d759444a..d9590c88b8 100644
--- a/actionpack/lib/action_controller/routing/builder.rb
+++ b/actionpack/lib/action_controller/routing/builder.rb
@@ -159,7 +159,8 @@ module ActionController
path = "/#{path}" unless path[0] == ?/
path = "#{path}/" unless path[-1] == ?/
- path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix]
+ prefix = options[:path_prefix].to_s.gsub(/^\//,'')
+ path = "/#{prefix}#{path}" unless prefix.blank?
segments = segments_for_route_path(path)
defaults, requirements, conditions = divide_route_options(segments, options)
diff --git a/actionpack/lib/action_controller/routing/recognition_optimisation.rb b/actionpack/lib/action_controller/routing/recognition_optimisation.rb
index ebc553512f..9bfebff0c0 100644
--- a/actionpack/lib/action_controller/routing/recognition_optimisation.rb
+++ b/actionpack/lib/action_controller/routing/recognition_optimisation.rb
@@ -98,7 +98,6 @@ module ActionController
if Array === item
i += 1
start = (i == 1)
- final = (i == list.size)
tag, sub = item
if tag == :dynamic
body += padding + "#{start ? 'if' : 'elsif'} true\n"
diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb
index 129e87c139..4f936d51d2 100644
--- a/actionpack/lib/action_controller/routing/segments.rb
+++ b/actionpack/lib/action_controller/routing/segments.rb
@@ -3,7 +3,11 @@ module ActionController
class Segment #:nodoc:
RESERVED_PCHAR = ':@&=+$,;'
SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
- UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
+ if RUBY_VERSION >= '1.9'
+ UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze
+ else
+ UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
+ end
# TODO: Convert :is_optional accessor to read only
attr_accessor :is_optional
@@ -314,13 +318,17 @@ module ActionController
end
def regexp_chunk
- '(\.[^/?\.]+)?'
+ '/|(\.[^/?\.]+)?'
end
def to_s
'(.:format)?'
end
-
+
+ def extract_value
+ "#{local_name} = options[:#{key}] && options[:#{key}].to_s.downcase"
+ end
+
#the value should not include the period (.)
def match_extraction(next_capture)
%[
diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb
index dbaec00bee..9dd09c30b4 100644
--- a/actionpack/lib/action_controller/test_process.rb
+++ b/actionpack/lib/action_controller/test_process.rb
@@ -258,11 +258,11 @@ module ActionController #:nodoc:
# Returns binary content (downloadable file), converted to a String
def binary_content
- raise "Response body is not a Proc: #{body.inspect}" unless body.kind_of?(Proc)
+ raise "Response body is not a Proc: #{body_parts.inspect}" unless body_parts.kind_of?(Proc)
require 'stringio'
sio = StringIO.new
- body.call(self, sio)
+ body_parts.call(self, sio)
sio.rewind
sio.read
diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb
index bb6cb437b7..16720b915b 100644
--- a/actionpack/lib/action_controller/url_rewriter.rb
+++ b/actionpack/lib/action_controller/url_rewriter.rb
@@ -68,29 +68,17 @@ module ActionController
# 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:
+ # you can do that by including ActionController::UrlWriter in your class:
#
# class User < ActiveRecord::Base
- # include ActionController::UrlWriter # !!!
+ # include ActionController::UrlWriter
#
- # def name=(value)
- # write_attribute('name', value)
- # write_attribute('base_uri', users_path) # !!!
+ # def base_uri
+ # user_path(self)
# end
# end
#
- # 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
+ # User.find(1).base_uri # => "/users/1"
module UrlWriter
def self.included(base) #:nodoc:
ActionController::Routing::Routes.install_helpers(base)
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 376bb87409..e2c49c284f 100644
--- a/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb
+++ b/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb
@@ -556,7 +556,7 @@ module HTML
end
# Attribute value.
- next if statement.sub!(/^\[\s*([[:alpha:]][\w\-]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do |match|
+ next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do |match|
name, equality, value = $1, $2, $3
if value == "?"
value = values.shift
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack.rb
index 6c03b55552..6349b95094 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack.rb
@@ -23,14 +23,16 @@ module Rack
# Return the Rack release as a dotted string.
def self.release
- "0.4"
+ "1.0 bundled"
end
autoload :Builder, "rack/builder"
autoload :Cascade, "rack/cascade"
+ autoload :Chunked, "rack/chunked"
autoload :CommonLogger, "rack/commonlogger"
autoload :ConditionalGet, "rack/conditionalget"
autoload :ContentLength, "rack/content_length"
+ autoload :ContentType, "rack/content_type"
autoload :File, "rack/file"
autoload :Deflater, "rack/deflater"
autoload :Directory, "rack/directory"
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/abstract/handler.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/abstract/handler.rb
index 8489c9b9c4..214df6299e 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/abstract/handler.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/abstract/handler.rb
@@ -8,8 +8,8 @@ module Rack
attr_accessor :realm
- def initialize(app, &authenticator)
- @app, @authenticator = app, authenticator
+ def initialize(app, realm=nil, &authenticator)
+ @app, @realm, @authenticator = app, realm, authenticator
end
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/digest/md5.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/digest/md5.rb
index 6d2bd29c2e..e579dc9632 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/digest/md5.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/digest/md5.rb
@@ -21,7 +21,7 @@ module Rack
attr_writer :passwords_hashed
- def initialize(app)
+ def initialize(*args)
super
@passwords_hashed = nil
end
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/digest/request.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/digest/request.rb
index a40f57b7f1..a8aa3bf996 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/digest/request.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/auth/digest/request.rb
@@ -8,7 +8,7 @@ module Rack
class Request < Auth::AbstractRequest
def method
- @env['REQUEST_METHOD']
+ @env['rack.methodoverride.original_method'] || @env['REQUEST_METHOD']
end
def digest?
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/builder.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/builder.rb
index 25994d5a44..295235e56a 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/builder.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/builder.rb
@@ -34,11 +34,7 @@ module Rack
end
def use(middleware, *args, &block)
- @ins << if block_given?
- lambda { |app| middleware.new(app, *args, &block) }
- else
- lambda { |app| middleware.new(app, *args) }
- end
+ @ins << lambda { |app| middleware.new(app, *args, &block) }
end
def run(app)
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/chunked.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/chunked.rb
new file mode 100644
index 0000000000..280d89dd65
--- /dev/null
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/chunked.rb
@@ -0,0 +1,49 @@
+require 'rack/utils'
+
+module Rack
+
+ # Middleware that applies chunked transfer encoding to response bodies
+ # when the response does not include a Content-Length header.
+ class Chunked
+ include Rack::Utils
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ status, headers, body = @app.call(env)
+ headers = HeaderHash.new(headers)
+
+ if env['HTTP_VERSION'] == 'HTTP/1.0' ||
+ STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
+ headers['Content-Length'] ||
+ headers['Transfer-Encoding']
+ [status, headers.to_hash, body]
+ else
+ dup.chunk(status, headers, body)
+ end
+ end
+
+ def chunk(status, headers, body)
+ @body = body
+ headers.delete('Content-Length')
+ headers['Transfer-Encoding'] = 'chunked'
+ [status, headers.to_hash, self]
+ end
+
+ def each
+ term = "\r\n"
+ @body.each do |chunk|
+ size = bytesize(chunk)
+ next if size == 0
+ yield [size.to_s(16), term, chunk, term].join
+ end
+ yield ["0", term, "", term].join
+ end
+
+ def close
+ @body.close if @body.respond_to?(:close)
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/content_length.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/content_length.rb
index bce22a32c5..1e56d43853 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/content_length.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/content_length.rb
@@ -3,21 +3,23 @@ require 'rack/utils'
module Rack
# Sets the Content-Length header on responses with fixed-length bodies.
class ContentLength
+ include Rack::Utils
+
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
- headers = Utils::HeaderHash.new(headers)
+ headers = HeaderHash.new(headers)
- if !Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) &&
+ if !STATUS_WITH_NO_ENTITY_BODY.include?(status) &&
!headers['Content-Length'] &&
!headers['Transfer-Encoding'] &&
(body.respond_to?(:to_ary) || body.respond_to?(:to_str))
body = [body] if body.respond_to?(:to_str) # rack 0.4 compat
- length = body.to_ary.inject(0) { |len, part| len + part.length }
+ length = body.to_ary.inject(0) { |len, part| len + bytesize(part) }
headers['Content-Length'] = length.to_s
end
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/content_type.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/content_type.rb
new file mode 100644
index 0000000000..0c1e1ca3e1
--- /dev/null
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/content_type.rb
@@ -0,0 +1,23 @@
+require 'rack/utils'
+
+module Rack
+
+ # Sets the Content-Type header on responses which don't have one.
+ #
+ # Builder Usage:
+ # use Rack::ContentType, "text/plain"
+ #
+ # When no content type argument is provided, "text/html" is assumed.
+ class ContentType
+ def initialize(app, content_type = "text/html")
+ @app, @content_type = app, content_type
+ end
+
+ def call(env)
+ status, headers, body = @app.call(env)
+ headers = Utils::HeaderHash.new(headers)
+ headers['Content-Type'] ||= @content_type
+ [status, headers.to_hash, body]
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/deflater.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/deflater.rb
index 3e66680092..a42b7477ae 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/deflater.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/deflater.rb
@@ -36,12 +36,12 @@ module Rack
mtime = headers.key?("Last-Modified") ?
Time.httpdate(headers["Last-Modified"]) : Time.now
body = self.class.gzip(body, mtime)
- size = body.respond_to?(:bytesize) ? body.bytesize : body.size
+ size = Rack::Utils.bytesize(body)
headers = headers.merge("Content-Encoding" => "gzip", "Content-Length" => size.to_s)
[status, headers, [body]]
when "deflate"
body = self.class.deflate(body)
- size = body.respond_to?(:bytesize) ? body.bytesize : body.size
+ size = Rack::Utils.bytesize(body)
headers = headers.merge("Content-Encoding" => "deflate", "Content-Length" => size.to_s)
[status, headers, [body]]
when "identity"
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/directory.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/directory.rb
index 56ee5e7b60..acdd3029d3 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/directory.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/directory.rb
@@ -70,7 +70,7 @@ table { width:100%%; }
return unless @path_info.include? ".."
body = "Forbidden\n"
- size = body.respond_to?(:bytesize) ? body.bytesize : body.size
+ size = Rack::Utils.bytesize(body)
return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]]
end
@@ -89,6 +89,8 @@ table { width:100%%; }
type = stat.directory? ? 'directory' : Mime.mime_type(ext)
size = stat.directory? ? '-' : filesize_format(size)
mtime = stat.mtime.httpdate
+ url << '/' if stat.directory?
+ basename << '/' if stat.directory?
@files << [ url, basename, size, type, mtime ]
end
@@ -120,7 +122,7 @@ table { width:100%%; }
def entity_not_found
body = "Entity not found: #{@path_info}\n"
- size = body.respond_to?(:bytesize) ? body.bytesize : body.size
+ size = Rack::Utils.bytesize(body)
return [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]]
end
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/file.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/file.rb
index 7869227a36..fe62bd6b86 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/file.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/file.rb
@@ -60,7 +60,7 @@ module Rack
body = self
else
body = [F.read(@path)]
- size = body.first.size
+ size = Utils.bytesize(body.first)
end
[200, {
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/cgi.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/cgi.rb
index f2c976cf46..e38156c7f0 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/cgi.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/cgi.rb
@@ -1,3 +1,5 @@
+require 'rack/content_length'
+
module Rack
module Handler
class CGI
@@ -6,6 +8,8 @@ module Rack
end
def self.serve(app)
+ app = ContentLength.new(app)
+
env = ENV.to_hash
env.delete "HTTP_CONTENT_LENGTH"
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/fastcgi.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/fastcgi.rb
index f03e1615c9..6324c7d274 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/fastcgi.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/fastcgi.rb
@@ -1,5 +1,6 @@
require 'fcgi'
require 'socket'
+require 'rack/content_length'
module Rack
module Handler
@@ -29,6 +30,8 @@ module Rack
end
def self.serve(request, app)
+ app = Rack::ContentLength.new(app)
+
env = request.env
env.delete "HTTP_CONTENT_LENGTH"
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/lsws.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/lsws.rb
index 1f850fc77b..c65ba3ec8e 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/lsws.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/lsws.rb
@@ -1,5 +1,6 @@
require 'lsapi'
-#require 'cgi'
+require 'rack/content_length'
+
module Rack
module Handler
class LSWS
@@ -9,11 +10,13 @@ module Rack
end
end
def self.serve(app)
+ app = Rack::ContentLength.new(app)
+
env = ENV.to_hash
env.delete "HTTP_CONTENT_LENGTH"
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({"rack.version" => [0,1],
- "rack.input" => $stdin,
+ "rack.input" => StringIO.new($stdin.read.to_s),
"rack.errors" => $stderr,
"rack.multithread" => false,
"rack.multiprocess" => true,
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/mongrel.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/mongrel.rb
index 178a1a8fe4..f0c0d58330 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/mongrel.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/mongrel.rb
@@ -1,5 +1,7 @@
require 'mongrel'
require 'stringio'
+require 'rack/content_length'
+require 'rack/chunked'
module Rack
module Handler
@@ -33,7 +35,7 @@ module Rack
end
def initialize(app)
- @app = app
+ @app = Rack::Chunked.new(Rack::ContentLength.new(app))
end
def process(request, response)
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/scgi.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/scgi.rb
index fd18a8359b..9495c66374 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/scgi.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/scgi.rb
@@ -1,5 +1,7 @@
require 'scgi'
require 'stringio'
+require 'rack/content_length'
+require 'rack/chunked'
module Rack
module Handler
@@ -14,7 +16,7 @@ module Rack
end
def initialize(settings = {})
- @app = settings[:app]
+ @app = Rack::Chunked.new(Rack::ContentLength.new(settings[:app]))
@log = Object.new
def @log.info(*args); end
def @log.error(*args); end
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/thin.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/thin.rb
index 7ad088b36a..3d4fedff75 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/thin.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/thin.rb
@@ -1,9 +1,12 @@
require "thin"
+require "rack/content_length"
+require "rack/chunked"
module Rack
module Handler
class Thin
def self.run(app, options={})
+ app = Rack::Chunked.new(Rack::ContentLength.new(app))
server = ::Thin::Server.new(options[:Host] || '0.0.0.0',
options[:Port] || 8080,
app)
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/webrick.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/webrick.rb
index 40be79de13..829e7d6bf8 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/webrick.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/handler/webrick.rb
@@ -1,5 +1,6 @@
require 'webrick'
require 'stringio'
+require 'rack/content_length'
module Rack
module Handler
@@ -14,7 +15,7 @@ module Rack
def initialize(server, app)
super server
- @app = app
+ @app = Rack::ContentLength.new(app)
end
def service(req, res)
@@ -35,7 +36,12 @@ module Rack
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
env["QUERY_STRING"] ||= ""
env["REQUEST_PATH"] ||= "/"
- env.delete "PATH_INFO" if env["PATH_INFO"] == ""
+ if env["PATH_INFO"] == ""
+ env.delete "PATH_INFO"
+ else
+ path, n = req.request_uri.path, env["SCRIPT_NAME"].length
+ env["PATH_INFO"] = path[n, path.length-n]
+ end
status, headers, body = @app.call(env)
begin
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/lint.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/lint.rb
index 7eb05437f0..44a33ce36e 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/lint.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/lint.rb
@@ -88,7 +88,9 @@ module Rack
## within the application. This may be an
## empty string, if the request URL targets
## the application root and does not have a
- ## trailing slash.
+ ## trailing slash. This information should be
+ ## decoded by the server if it comes from a
+ ## URL.
## <tt>QUERY_STRING</tt>:: The portion of the request URL that
## follows the <tt>?</tt>, if any. May be
@@ -372,59 +374,43 @@ module Rack
## === The Content-Length
def check_content_length(status, headers, env)
- chunked_response = false
- headers.each { |key, value|
- if key.downcase == 'transfer-encoding'
- chunked_response = value.downcase != 'identity'
- end
- }
-
headers.each { |key, value|
if key.downcase == 'content-length'
- ## There must be a <tt>Content-Length</tt>, except when the
- ## +Status+ is 1xx, 204 or 304, in which case there must be none
- ## given.
+ ## There must not be a <tt>Content-Length</tt> header when the
+ ## +Status+ is 1xx, 204 or 304.
assert("Content-Length header found in #{status} response, not allowed") {
not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
}
- assert('Content-Length header should not be used if body is chunked') {
- not chunked_response
- }
-
bytes = 0
string_body = true
- @body.each { |part|
- unless part.kind_of?(String)
- string_body = false
- break
- end
+ if @body.respond_to?(:to_ary)
+ @body.each { |part|
+ unless part.kind_of?(String)
+ string_body = false
+ break
+ end
- bytes += (part.respond_to?(:bytesize) ? part.bytesize : part.size)
- }
-
- if env["REQUEST_METHOD"] == "HEAD"
- assert("Response body was given for HEAD request, but should be empty") {
- bytes == 0
+ bytes += Rack::Utils.bytesize(part)
}
- else
- if string_body
- assert("Content-Length header was #{value}, but should be #{bytes}") {
- value == bytes.to_s
+
+ if env["REQUEST_METHOD"] == "HEAD"
+ assert("Response body was given for HEAD request, but should be empty") {
+ bytes == 0
}
+ else
+ if string_body
+ assert("Content-Length header was #{value}, but should be #{bytes}") {
+ value == bytes.to_s
+ }
+ end
end
end
return
end
}
-
- if [ String, Array ].include?(@body.class) && !chunked_response
- assert('No Content-Length header found') {
- Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
- }
- end
end
## === The Body
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/response.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/response.rb
index a593110139..caf60d5b19 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/response.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/response.rb
@@ -16,6 +16,8 @@ module Rack
# Your application's +call+ should end returning Response#finish.
class Response
+ attr_accessor :length
+
def initialize(body=[], status=200, header={}, &block)
@status = status
@header = Utils::HeaderHash.new({"Content-Type" => "text/html"}.
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/showstatus.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/showstatus.rb
index 5f13404dce..28258c7c89 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/showstatus.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/showstatus.rb
@@ -27,7 +27,7 @@ module Rack
message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s
detail = env["rack.showstatus.detail"] || message
body = @template.result(binding)
- size = body.respond_to?(:bytesize) ? body.bytesize : body.size
+ size = Rack::Utils.bytesize(body)
[status, headers.merge("Content-Type" => "text/html", "Content-Length" => size.to_s), [body]]
else
[status, headers, body]
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/urlmap.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/urlmap.rb
index eb1457a850..0ff32df181 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/urlmap.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/urlmap.rb
@@ -12,7 +12,11 @@ module Rack
# first, since they are most specific.
class URLMap
- def initialize(map)
+ def initialize(map = {})
+ remap(map)
+ end
+
+ def remap(map)
@mapping = map.map { |location, app|
if location =~ %r{\Ahttps?://(.*?)(/.*)}
host, location = $1, $2
diff --git a/actionpack/lib/action_controller/vendor/rack-1.0/rack/utils.rb b/actionpack/lib/action_controller/vendor/rack-1.0/rack/utils.rb
index f352cb6783..0a61bce707 100644
--- a/actionpack/lib/action_controller/vendor/rack-1.0/rack/utils.rb
+++ b/actionpack/lib/action_controller/vendor/rack-1.0/rack/utils.rb
@@ -144,6 +144,19 @@ module Rack
end
module_function :select_best_encoding
+ # Return the bytesize of String; uses String#length under Ruby 1.8 and
+ # String#bytesize under 1.9.
+ if ''.respond_to?(:bytesize)
+ def bytesize(string)
+ string.bytesize
+ end
+ else
+ def bytesize(string)
+ string.size
+ end
+ end
+ module_function :bytesize
+
# Context allows the use of a compatible middleware at different points
# in a request handling stack. A compatible middleware must define
# #context which should take the arguments env and app. The first of which
@@ -359,7 +372,7 @@ module Rack
data = body
end
- Utils.normalize_params(params, name, data)
+ Utils.normalize_params(params, name, data) unless data.nil?
break if buf.empty? || content_length == -1
}
diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb
index f20e44a7d5..e0aa2a5f2f 100644
--- a/actionpack/lib/action_pack/version.rb
+++ b/actionpack/lib/action_pack/version.rb
@@ -2,7 +2,7 @@ module ActionPack #:nodoc:
module VERSION #:nodoc:
MAJOR = 2
MINOR = 3
- TINY = 0
+ TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end
diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb
index 65b2062337..9c0134e7f7 100644
--- a/actionpack/lib/action_view/base.rb
+++ b/actionpack/lib/action_view/base.rb
@@ -183,12 +183,12 @@ module ActionView #:nodoc:
cattr_accessor :debug_rjs
# Specify whether templates should be cached. Otherwise the file we be read everytime it is accessed.
- # Automaticaly reloading templates are not thread safe and should only be used in development mode.
- @@cache_template_loading = false
+ # Automatically reloading templates are not thread safe and should only be used in development mode.
+ @@cache_template_loading = nil
cattr_accessor :cache_template_loading
def self.cache_template_loading?
- ActionController::Base.allow_concurrency || cache_template_loading
+ ActionController::Base.allow_concurrency || (cache_template_loading.nil? ? !ActiveSupport::Dependencies.load? : cache_template_loading)
end
attr_internal :request
@@ -221,10 +221,12 @@ module ActionView #:nodoc:
def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)#:nodoc:
@assigns = assigns_for_first_render
@assigns_added = nil
- @_render_stack = []
@controller = controller
@helpers = ProxyModule.new(self)
self.view_paths = view_paths
+
+ @_first_render = nil
+ @_current_render = nil
end
attr_reader :view_paths
@@ -286,7 +288,25 @@ module ActionView #:nodoc:
# Access the current template being rendered.
# Returns a ActionView::Template object.
def template
- @_render_stack.last
+ @_current_render
+ end
+
+ def template=(template) #:nodoc:
+ @_first_render ||= template
+ @_current_render = template
+ end
+
+ def with_template(current_template)
+ last_template, self.template = template, current_template
+ yield
+ ensure
+ self.template = last_template
+ end
+
+ def punctuate_body!(part)
+ flush_output_buffer
+ response.body_parts << part
+ nil
end
private
diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb
index 8b56d241ae..541899ea6a 100644
--- a/actionpack/lib/action_view/helpers/active_record_helper.rb
+++ b/actionpack/lib/action_view/helpers/active_record_helper.rb
@@ -121,7 +121,7 @@ module ActionView
if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) &&
(errors = obj.errors.on(method))
content_tag("div",
- "#{options[:prepend_text]}#{errors.is_a?(Array) ? errors.first : errors}#{options[:append_text]}",
+ "#{options[:prepend_text]}#{ERB::Util.html_escape(errors.is_a?(Array) ? errors.first : errors)}#{options[:append_text]}",
:class => options[:css_class]
)
else
@@ -198,7 +198,7 @@ module ActionView
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
+ error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, ERB::Util.html_escape(msg)) } }.join
contents = ''
contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb
index e86ca27f31..9e39536653 100644
--- a/actionpack/lib/action_view/helpers/capture_helper.rb
+++ b/actionpack/lib/action_view/helpers/capture_helper.rb
@@ -131,6 +131,14 @@ module ActionView
ensure
self.output_buffer = old_buffer
end
+
+ # Add the output buffer to the response body and start a new one.
+ def flush_output_buffer #:nodoc:
+ if output_buffer && output_buffer != ''
+ response.body_parts << output_buffer
+ self.output_buffer = ''
+ end
+ 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 b7ef1fb90d..c74909a360 100644
--- a/actionpack/lib/action_view/helpers/date_helper.rb
+++ b/actionpack/lib/action_view/helpers/date_helper.rb
@@ -876,8 +876,8 @@ module ActionView
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
+ # Given an ordering of datetime components, create the selection HTML
+ # and join them with their appropriate separators.
def build_selects_from_types(order)
select = ''
order.reverse.each do |type|
diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb
index 4fef2b443e..a59829b23f 100644
--- a/actionpack/lib/action_view/helpers/form_helper.rb
+++ b/actionpack/lib/action_view/helpers/form_helper.rb
@@ -5,17 +5,24 @@ require 'action_view/helpers/form_tag_helper'
module ActionView
module Helpers
- # Form helpers are designed to make working with models much easier compared to using just standard HTML
- # elements by providing a set of methods for creating forms based on your models. This helper generates the HTML
- # for forms, providing a method for each sort of input (e.g., text, password, select, and so on). When the form
- # is submitted (i.e., when the user hits the submit button or <tt>form.submit</tt> is called via JavaScript), the form inputs will be bundled into the <tt>params</tt> object and passed back to the controller.
+ # Form helpers are designed to make working with models much easier
+ # compared to using just standard HTML elements by providing a set of
+ # methods for creating forms based on your models. This helper generates
+ # the HTML for forms, providing a method for each sort of input
+ # (e.g., text, password, select, and so on). When the form is submitted
+ # (i.e., when the user hits the submit button or <tt>form.submit</tt> is
+ # called via JavaScript), the form inputs will be bundled into the
+ # <tt>params</tt> object and passed back to the controller.
#
- # There are two types of form helpers: those that specifically work with model attributes and those that don't.
- # This helper deals with those that work with model attributes; to see an example of form helpers that don't work
- # with model attributes, check the ActionView::Helpers::FormTagHelper documentation.
+ # There are two types of form helpers: those that specifically work with
+ # model attributes and those that don't. This helper deals with those that
+ # work with model attributes; to see an example of form helpers that don't
+ # work with model attributes, check the ActionView::Helpers::FormTagHelper
+ # documentation.
#
- # The core method of this helper, form_for, gives you the ability to create a form for a model instance;
- # for example, let's say that you have a model <tt>Person</tt> and want to create a new instance of it:
+ # The core method of this helper, form_for, gives you the ability to create
+ # a form for a model instance; for example, let's say that you have a model
+ # <tt>Person</tt> and want to create a new instance of it:
#
# # Note: a @person variable will have been created in the controller.
# # For example: @person = Person.new
@@ -40,17 +47,22 @@ module ActionView
# <%= submit_tag 'Create' %>
# <% end %>
#
- # This example will render the <tt>people/_form</tt> partial, setting a local variable called <tt>form</tt> which references the yielded FormBuilder.
- #
- # The <tt>params</tt> object created when this form is submitted would look like:
+ # This example will render the <tt>people/_form</tt> partial, setting a
+ # local variable called <tt>form</tt> which references the yielded
+ # FormBuilder. The <tt>params</tt> object created when this form is
+ # submitted would look like:
#
# {"action"=>"create", "controller"=>"persons", "person"=>{"first_name"=>"William", "last_name"=>"Smith"}}
#
- # The params hash has a nested <tt>person</tt> value, which can therefore be accessed with <tt>params[:person]</tt> in the controller.
- # If were editing/updating an instance (e.g., <tt>Person.find(1)</tt> rather than <tt>Person.new</tt> in the controller), the objects
- # attribute values are filled into the form (e.g., the <tt>person_first_name</tt> field would have that person's first name in it).
+ # The params hash has a nested <tt>person</tt> value, which can therefore
+ # be accessed with <tt>params[:person]</tt> in the controller. If were
+ # editing/updating an instance (e.g., <tt>Person.find(1)</tt> rather than
+ # <tt>Person.new</tt> in the controller), the objects attribute values are
+ # filled into the form (e.g., the <tt>person_first_name</tt> field would
+ # have that person's first name in it).
#
- # If the object name contains square brackets the id for the object will be inserted. For example:
+ # If the object name contains square brackets the id for the object will be
+ # inserted. For example:
#
# <%= text_field "person[]", "name" %>
#
@@ -58,8 +70,10 @@ module ActionView
#
# <input type="text" id="person_<%= @person.id %>_name" name="person[<%= @person.id %>][name]" value="<%= @person.name %>" />
#
- # If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial
- # used by <tt>render_collection_of_partials</tt>, the <tt>index</tt> option may come in handy. Example:
+ # If the helper is being used to generate a repetitive sequence of similar
+ # form elements, for example in a partial used by
+ # <tt>render_collection_of_partials</tt>, the <tt>index</tt> option may
+ # come in handy. Example:
#
# <%= text_field "person", "name", "index" => 1 %>
#
@@ -67,14 +81,17 @@ module ActionView
#
# <input type="text" id="person_1_name" name="person[1][name]" value="<%= @person.name %>" />
#
- # An <tt>index</tt> option may also be passed to <tt>form_for</tt> and <tt>fields_for</tt>. This automatically applies
- # the <tt>index</tt> to all the nested fields.
+ # An <tt>index</tt> option may also be passed to <tt>form_for</tt> and
+ # <tt>fields_for</tt>. This automatically applies the <tt>index</tt> to
+ # all the nested fields.
#
- # There are also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html,
- # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html
+ # There are also methods for helping to build form tags in
+ # link:classes/ActionView/Helpers/FormOptionsHelper.html,
+ # link:classes/ActionView/Helpers/DateHelper.html, and
+ # link:classes/ActionView/Helpers/ActiveRecordHelper.html
module FormHelper
- # Creates a form and a scope around a specific model object that is used as
- # a base for questioning about values for the fields.
+ # 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 succinct resource-oriented form generation with +form_for+
# like this:
@@ -86,13 +103,15 @@ module ActionView
# <%= f.text_field :author %><br />
# <% end %>
#
- # There, +form_for+ is able to generate the rest of RESTful form parameters
- # based on introspection on the record, but to understand what it does we
- # need to dig first into the alternative generic usage it is based upon.
+ # There, +form_for+ is able to generate the rest of RESTful form
+ # parameters based on introspection on the record, but to understand what
+ # it does we need to dig first into the alternative generic usage it is
+ # based upon.
#
# === Generic form_for
#
- # The generic way to call +form_for+ yields a form builder around a model:
+ # The generic way to call +form_for+ yields a form builder around a
+ # model:
#
# <% form_for :person, :url => { :action => "update" } do |f| %>
# <%= f.error_messages %>
@@ -103,8 +122,8 @@ module ActionView
# <% end %>
#
# There, the first argument is a symbol or string with the name of the
- # object the form is about, and also the name of the instance variable the
- # object is stored in.
+ # object the form is about, and also the name of the instance variable
+ # the object is stored in.
#
# The form builder acts as a regular form helper that somehow carries the
# model. Thus, the idea is that
@@ -137,17 +156,18 @@ module ActionView
# In any of its variants, the rightmost argument to +form_for+ is an
# optional hash of options:
#
- # * <tt>:url</tt> - The URL the form is submitted to. It takes the same fields
- # you pass to +url_for+ or +link_to+. In particular you may pass here a
- # named route directly as well. Defaults to the current action.
+ # * <tt>:url</tt> - The URL the form is submitted to. It takes the same
+ # fields you pass to +url_for+ or +link_to+. In particular you may pass
+ # here a named route directly as well. Defaults to the current action.
# * <tt>:html</tt> - Optional HTML attributes for the form tag.
#
- # Worth noting is that the +form_for+ tag is called in a ERb evaluation block,
- # not an ERb output block. So that's <tt><% %></tt>, not <tt><%= %></tt>.
+ # Worth noting is that the +form_for+ tag is called in a ERb evaluation
+ # block, not an ERb output block. So that's <tt><% %></tt>, not
+ # <tt><%= %></tt>.
#
# Also note that +form_for+ doesn't create an exclusive scope. It's still
- # possible to use both the stand-alone FormHelper methods and methods from
- # FormTagHelper. For example:
+ # possible to use both the stand-alone FormHelper methods and methods
+ # from FormTagHelper. For example:
#
# <% form_for :person, @person, :url => { :action => "update" } do |f| %>
# First name: <%= f.text_field :first_name %>
@@ -156,16 +176,16 @@ module ActionView
# Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %>
# <% end %>
#
- # This also works for the methods in FormOptionHelper and DateHelper that are
- # designed to work with an object as base, like FormOptionHelper#collection_select
- # and DateHelper#datetime_select.
+ # This also works for the methods in FormOptionHelper and DateHelper that
+ # are designed to work with an object as base, like
+ # FormOptionHelper#collection_select and DateHelper#datetime_select.
#
# === Resource-oriented style
#
- # As we said above, in addition to manually configuring the +form_for+ call,
- # you can rely on automated resource identification, which will use the conventions
- # and named routes of that approach. This is the preferred way to use +form_for+
- # nowadays.
+ # As we said above, in addition to manually configuring the +form_for+
+ # call, you can rely on automated resource identification, which will use
+ # the conventions and named routes of that approach. This is the
+ # preferred way to use +form_for+ nowadays.
#
# For example, if <tt>@post</tt> is an existing record you want to edit
#
@@ -205,8 +225,10 @@ module ActionView
#
# === Customized form builders
#
- # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers,
- # then use your custom builder. For example, let's say you made a helper to automatically add labels to form inputs.
+ # You can also build forms using a customized FormBuilder class. Subclass
+ # FormBuilder and override or define some more helpers, then use your
+ # custom builder. For example, let's say you made a helper to
+ # automatically add labels to form inputs.
#
# <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %>
# <%= f.text_field :first_name %>
@@ -219,16 +241,23 @@ module ActionView
#
# <%= render :partial => f %>
#
- # The rendered template is <tt>people/_labelling_form</tt> and the local variable referencing the form builder is called <tt>labelling_form</tt>.
+ # The rendered template is <tt>people/_labelling_form</tt> and the local
+ # variable referencing the form builder is called
+ # <tt>labelling_form</tt>.
#
- # In many cases you will want to wrap the above in another helper, so you could do something like the following:
+ # The custom FormBuilder class is automatically merged with the options
+ # of a nested fields_for call, unless it's explicitely set.
+ #
+ # In many cases you will want to wrap the above in another helper, so you
+ # could do something like the following:
#
# def labelled_form_for(record_or_name_or_array, *args, &proc)
# options = args.extract_options!
# form_for(record_or_name_or_array, *(args << options.merge(:builder => LabellingFormBuilder)), &proc)
# end
#
- # If you don't need to attach a form to a model instance, then check out FormTagHelper#form_tag.
+ # If you don't need to attach a form to a model instance, then check out
+ # FormTagHelper#form_tag.
def form_for(record_or_name_or_array, *args, &proc)
raise ArgumentError, "Missing block" unless block_given?
@@ -599,7 +628,7 @@ module ActionView
#
# 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
+ # 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
#
@@ -607,12 +636,15 @@ module ActionView
#
# 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.
+ # To prevent this the helper generates an auxiliary hidden field before
+ # the very check box. The hidden field has the same name and its
+ # attributes mimick an unchecked check box.
+ #
+ # This way, 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 parameters extraction gets the last occurrence of any repeated
+ # key in the query string, that works for ordinary forms.
#
# Unfortunately that workaround does not work when the check box goes
# within an array-like parameter, as in
@@ -623,22 +655,26 @@ module ActionView
# <% end %>
#
# because parameter name repetition is precisely what Rails seeks to distinguish
- # the elements of the array.
+ # the elements of the array. For each item with a checked check box you
+ # get an extra ghost item with only that attribute, assigned to "0".
+ #
+ # In that case it is preferable to either use +check_box_tag+ or to use
+ # hashes instead of arrays.
#
# ==== Examples
# # Let's say that @post.validated? is 1:
# check_box("post", "validated")
- # # => <input type="checkbox" id="post_validated" name="post[validated]" value="1" />
- # # <input name="post[validated]" type="hidden" value="0" />
+ # # => <input name="post[validated]" type="hidden" value="0" />
+ # # <input type="checkbox" id="post_validated" name="post[validated]" value="1" />
#
# # Let's say that @puppy.gooddog is "no":
# check_box("puppy", "gooddog", {}, "yes", "no")
- # # => <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
- # # <input name="puppy[gooddog]" type="hidden" value="no" />
+ # # => <input name="puppy[gooddog]" type="hidden" value="no" />
+ # # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
#
# check_box("eula", "accepted", { :class => 'eula_check' }, "yes", "no")
- # # => <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
- # # <input name="eula[accepted]" type="hidden" value="no" />
+ # # => <input name="eula[accepted]" type="hidden" value="no" />
+ # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
#
def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
InstanceTag.new(object_name, method, self, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value)
@@ -910,6 +946,11 @@ module ActionView
index = ""
end
+ if options[:builder]
+ args << {} unless args.last.is_a?(Hash)
+ args.last[:builder] ||= options[:builder]
+ end
+
case record_or_name_or_array
when String, Symbol
if nested_attributes_association?(record_or_name_or_array)
diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb
index 4646bc118b..6d39a53adc 100644
--- a/actionpack/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb
@@ -360,8 +360,8 @@ module ActionView
end
if confirm = options.delete("confirm")
- options["onclick"] ||= ''
- options["onclick"] << "return #{confirm_javascript_function(confirm)};"
+ options["onclick"] ||= 'return true;'
+ options["onclick"] = "if (!#{confirm_javascript_function(confirm)}) return false; #{options['onclick']}"
end
tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys)
diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb
index e622f97b9e..dea958deaf 100644
--- a/actionpack/lib/action_view/helpers/number_helper.rb
+++ b/actionpack/lib/action_view/helpers/number_helper.rb
@@ -15,6 +15,7 @@ module ActionView
# * <tt>:country_code</tt> - Sets the country code for the phone number.
#
# ==== Examples
+ # number_to_phone(5551234) # => 555-1234
# number_to_phone(1235551234) # => 123-555-1234
# number_to_phone(1235551234, :area_code => true) # => (123) 555-1234
# number_to_phone(1235551234, :delimiter => " ") # => 123 555 1234
@@ -37,7 +38,8 @@ module ActionView
str << if area_code
number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4}$)/,"(\\1) \\2#{delimiter}\\3")
else
- number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
+ number.gsub!(/([0-9]{0,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
+ number.starts_with?('-') ? number.slice!(1..-1) : number
end
str << " x #{extension}" unless extension.blank?
str
@@ -138,7 +140,7 @@ module ActionView
# 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(12345678, :separator => ",") # => 12,345,678
# number_with_delimiter(98765432.98, :delimiter => " ", :separator => ",")
# # => 98 765 432,98
#
diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb
index 18a209dcea..91ef72e54b 100644
--- a/actionpack/lib/action_view/helpers/prototype_helper.rb
+++ b/actionpack/lib/action_view/helpers/prototype_helper.rb
@@ -107,7 +107,7 @@ module ActionView
# on the page in an Ajax response.
module PrototypeHelper
unless const_defined? :CALLBACKS
- CALLBACKS = Set.new([ :uninitialized, :loading, :loaded,
+ CALLBACKS = Set.new([ :create, :uninitialized, :loading, :loaded,
:interactive, :complete, :failure, :success ] +
(100..599).to_a)
AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url,
diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb
index 63fe0c1c57..573b99b96e 100644
--- a/actionpack/lib/action_view/helpers/text_helper.rb
+++ b/actionpack/lib/action_view/helpers/text_helper.rb
@@ -324,7 +324,7 @@ module ActionView
# Turns all URLs and e-mail addresses into clickable links. The <tt>:link</tt> option
# will limit what should be linked. You can add HTML attributes to the links using
- # <tt>:href_options</tt>. Possible values for <tt>:link</tt> are <tt>:all</tt> (default),
+ # <tt>:html</tt>. Possible values for <tt>:link</tt> are <tt>:all</tt> (default),
# <tt>:email_addresses</tt>, and <tt>:urls</tt>. If a block is given, each URL and
# e-mail address is yielded and the result is used as the link text.
#
@@ -341,7 +341,7 @@ module ActionView
# # => "Visit http://www.loudthinking.com/ or e-mail <a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>"
#
# post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com."
- # auto_link(post_body, :href_options => { :target => '_blank' }) do |text|
+ # auto_link(post_body, :html => { :target => '_blank' }) do |text|
# truncate(text, 15)
# end
# # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.m...</a>.
@@ -359,7 +359,7 @@ module ActionView
# 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)
+ def auto_link(text, *args, &block)#link = :all, html = {}, &block)
return '' if text.blank?
options = args.size == 2 ? {} : args.extract_options! # this is necessary because the old auto_link API has a Hash as its last parameter
@@ -536,8 +536,9 @@ module ActionView
text.gsub(AUTO_LINK_RE) do
href = $&
punctuation = ''
- # detect already linked URLs
- if $` =~ /<a\s[^>]*href="$/
+ left, right = $`, $'
+ # detect already linked URLs and URLs in the middle of a tag
+ if left =~ /<[^>]+$/ && right =~ /^[^>]*>/
# do not change string; URL is alreay linked
href
else
diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb
index 41f9f486e5..8cc3fe291c 100644
--- a/actionpack/lib/action_view/paths.rb
+++ b/actionpack/lib/action_view/paths.rb
@@ -40,7 +40,7 @@ module ActionView #:nodoc:
each(&:load!)
end
- def find_template(original_template_path, format = nil)
+ def find_template(original_template_path, format = nil, html_fallback = true)
return original_template_path if original_template_path.respond_to?(:render)
template_path = original_template_path.sub(/^\//, '')
@@ -54,14 +54,14 @@ module ActionView #:nodoc:
elsif template = load_path[template_path]
return template
# Try to find html version if the format is javascript
- elsif format == :js && template = load_path["#{template_path}.#{I18n.locale}.html"]
+ elsif format == :js && html_fallback && template = load_path["#{template_path}.#{I18n.locale}.html"]
return template
- elsif format == :js && template = load_path["#{template_path}.html"]
+ elsif format == :js && html_fallback && template = load_path["#{template_path}.html"]
return template
end
end
- return Template.new(original_template_path, original_template_path =~ /\A\// ? "" : ".") if File.file?(original_template_path)
+ return Template.new(original_template_path, original_template_path.to_s =~ /\A\// ? "" : ".") if File.file?(original_template_path)
raise MissingTemplate.new(self, original_template_path, format)
end
diff --git a/actionpack/lib/action_view/renderable.rb b/actionpack/lib/action_view/renderable.rb
index 41080ed629..ff7bc7d9de 100644
--- a/actionpack/lib/action_view/renderable.rb
+++ b/actionpack/lib/action_view/renderable.rb
@@ -27,23 +27,19 @@ module ActionView
def render(view, local_assigns = {})
compile(local_assigns)
- stack = view.instance_variable_get(:@_render_stack)
- stack.push(self)
-
- view.send(:_evaluate_assigns_and_ivars)
- view.send(:_set_controller_content_type, mime_type) if respond_to?(:mime_type)
-
- result = view.send(method_name(local_assigns), local_assigns) do |*names|
- ivar = :@_proc_for_layout
- if !view.instance_variable_defined?(:"@content_for_#{names.first}") && view.instance_variable_defined?(ivar) && (proc = view.instance_variable_get(ivar))
- view.capture(*names, &proc)
- elsif view.instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}")
- view.instance_variable_get(ivar)
+ view.with_template self do
+ view.send(:_evaluate_assigns_and_ivars)
+ view.send(:_set_controller_content_type, mime_type) if respond_to?(:mime_type)
+
+ view.send(method_name(local_assigns), local_assigns) do |*names|
+ ivar = :@_proc_for_layout
+ if !view.instance_variable_defined?(:"@content_for_#{names.first}") && view.instance_variable_defined?(ivar) && (proc = view.instance_variable_get(ivar))
+ view.capture(*names, &proc)
+ elsif view.instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}")
+ view.instance_variable_get(ivar)
+ end
end
end
-
- stack.pop
- result
end
def method_name(local_assigns)
diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb
index ea838b9b02..c339c8a554 100644
--- a/actionpack/lib/action_view/template.rb
+++ b/actionpack/lib/action_view/template.rb
@@ -103,12 +103,12 @@ module ActionView #:nodoc:
@@exempt_from_layout.merge(regexps)
end
- attr_accessor :filename, :load_path, :base_path
+ attr_accessor :template_path, :filename, :load_path, :base_path
attr_accessor :locale, :name, :format, :extension
delegate :to_s, :to => :path
def initialize(template_path, load_path)
- template_path = template_path.dup
+ @template_path = template_path.dup
@load_path, @filename = load_path, File.join(load_path, template_path)
@base_path, @name, @locale, @format, @extension = split(template_path)
@base_path.to_s.gsub!(/\/$/, '') # Push to split method
@@ -119,13 +119,20 @@ module ActionView #:nodoc:
def accessible_paths
paths = []
- paths << path
- paths << path_without_extension
- if multipart?
- formats = format.split(".")
- paths << "#{path_without_format_and_extension}.#{formats.first}"
- paths << "#{path_without_format_and_extension}.#{formats.second}"
+
+ if valid_extension?(extension)
+ paths << path
+ paths << path_without_extension
+ if multipart?
+ formats = format.split(".")
+ paths << "#{path_without_format_and_extension}.#{formats.first}"
+ paths << "#{path_without_format_and_extension}.#{formats.second}"
+ end
+ else
+ # template without explicit template handler should only be reachable through its exact path
+ paths << template_path
end
+
paths
end
@@ -211,7 +218,7 @@ module ActionView #:nodoc:
# Returns file split into an array
# [base_path, name, locale, format, extension]
def split(file)
- if m = file.match(/^(.*\/)?([^\.]+)\.(.*)$/)
+ if m = file.to_s.match(/^(.*\/)?([^\.]+)\.(.*)$/)
base_path = m[1]
name = m[2]
extensions = m[3]
diff --git a/actionpack/test/activerecord/active_record_store_test.rb b/actionpack/test/activerecord/active_record_store_test.rb
index 6a75e6050d..c98892edc1 100644
--- a/actionpack/test/activerecord/active_record_store_test.rb
+++ b/actionpack/test/activerecord/active_record_store_test.rb
@@ -21,6 +21,18 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest
render :text => "foo: #{session[:foo].inspect}"
end
+ def get_session_id
+ session[:foo]
+ render :text => "#{request.session_options[:id]}"
+ end
+
+ def call_reset_session
+ session[:bar]
+ reset_session
+ session[:bar] = "baz"
+ head :ok
+ end
+
def rescue_action(e) raise end
end
@@ -61,6 +73,40 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest
end
end
+ def test_setting_session_value_after_session_reset
+ with_test_route_set do
+ get '/set_session_value'
+ assert_response :success
+ assert cookies['_session_id']
+ session_id = cookies['_session_id']
+
+ get '/call_reset_session'
+ assert_response :success
+ assert_not_equal [], headers['Set-Cookie']
+
+ get '/get_session_value'
+ assert_response :success
+ assert_equal 'foo: nil', response.body
+
+ get '/get_session_id'
+ assert_response :success
+ assert_not_equal session_id, response.body
+ end
+ end
+
+ def test_getting_session_id
+ with_test_route_set do
+ get '/set_session_value'
+ assert_response :success
+ assert cookies['_session_id']
+ session_id = cookies['_session_id']
+
+ get '/get_session_id'
+ assert_response :success
+ assert_equal session_id, response.body
+ end
+ end
+
def test_prevents_session_fixation
with_test_route_set do
get '/set_session_value'
diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb
index 99c57c0c91..298c7e4db3 100644
--- a/actionpack/test/controller/assert_select_test.rb
+++ b/actionpack/test/controller/assert_select_test.rb
@@ -76,7 +76,7 @@ class AssertSelectTest < ActionController::TestCase
end
def assert_failure(message, &block)
- e = assert_raises(Assertion, &block)
+ e = assert_raise(Assertion, &block)
assert_match(message, e.message) if Regexp === message
assert_equal(message, e.message) if String === message
end
@@ -95,24 +95,24 @@ class AssertSelectTest < ActionController::TestCase
def test_equality_true_false
render_html %Q{<div id="1"></div><div id="2"></div>}
assert_nothing_raised { assert_select "div" }
- assert_raises(Assertion) { assert_select "p" }
+ assert_raise(Assertion) { assert_select "p" }
assert_nothing_raised { assert_select "div", true }
- assert_raises(Assertion) { assert_select "p", true }
- assert_raises(Assertion) { assert_select "div", false }
+ assert_raise(Assertion) { assert_select "p", true }
+ assert_raise(Assertion) { assert_select "div", false }
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" }
- assert_raises(Assertion) { assert_select "div", "bar" }
+ assert_raise(Assertion) { assert_select "div", "bar" }
assert_nothing_raised { assert_select "div", :text=>"foo" }
- assert_raises(Assertion) { assert_select "div", :text=>"bar" }
+ assert_raise(Assertion) { assert_select "div", :text=>"bar" }
assert_nothing_raised { assert_select "div", /(foo|bar)/ }
- assert_raises(Assertion) { assert_select "div", /foobar/ }
+ assert_raise(Assertion) { assert_select "div", /foobar/ }
assert_nothing_raised { assert_select "div", :text=>/(foo|bar)/ }
- assert_raises(Assertion) { assert_select "div", :text=>/foobar/ }
- assert_raises(Assertion) { assert_select "p", :text=>/foobar/ }
+ assert_raise(Assertion) { assert_select "div", :text=>/foobar/ }
+ assert_raise(Assertion) { assert_select "p", :text=>/foobar/ }
end
def test_equality_of_html
@@ -120,17 +120,17 @@ class AssertSelectTest < ActionController::TestCase
text = "\"This is not a big problem,\" he said."
html = "<em>\"This is <strong>not</strong> a big problem,\"</em> he said."
assert_nothing_raised { assert_select "p", text }
- assert_raises(Assertion) { assert_select "p", html }
+ assert_raise(Assertion) { assert_select "p", html }
assert_nothing_raised { assert_select "p", :html=>html }
- assert_raises(Assertion) { assert_select "p", :html=>text }
+ assert_raise(Assertion) { assert_select "p", :html=>text }
# No stripping for pre.
render_html %Q{<pre>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</pre>}
text = "\n\"This is not a big problem,\" he said.\n"
html = "\n<em>\"This is <strong>not</strong> a big problem,\"</em> he said.\n"
assert_nothing_raised { assert_select "pre", text }
- assert_raises(Assertion) { assert_select "pre", html }
+ assert_raise(Assertion) { assert_select "pre", html }
assert_nothing_raised { assert_select "pre", :html=>html }
- assert_raises(Assertion) { assert_select "pre", :html=>text }
+ assert_raise(Assertion) { assert_select "pre", :html=>text }
end
def test_counts
@@ -210,12 +210,12 @@ class AssertSelectTest < ActionController::TestCase
assert_nothing_raised { assert_select "div", "bar" }
assert_nothing_raised { assert_select "div", /\w*/ }
assert_nothing_raised { assert_select "div", /\w*/, :count=>2 }
- assert_raises(Assertion) { assert_select "div", :text=>"foo", :count=>2 }
+ assert_raise(Assertion) { assert_select "div", :text=>"foo", :count=>2 }
assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" }
assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" }
assert_nothing_raised { assert_select "div", :html=>/\w*/ }
assert_nothing_raised { assert_select "div", :html=>/\w*/, :count=>2 }
- assert_raises(Assertion) { assert_select "div", :html=>"<span>foo</span>", :count=>2 }
+ assert_raise(Assertion) { assert_select "div", :html=>"<span>foo</span>", :count=>2 }
end
end
@@ -253,7 +253,12 @@ class AssertSelectTest < ActionController::TestCase
page.insert_html :top, "test1", "<div id=\"1\">foo</div>"
page.insert_html :bottom, "test2", "<div id=\"2\">foo</div>"
end
- assert_raises(Assertion) {assert_select_rjs :insert, :top, "test2"}
+ assert_raise(Assertion) {assert_select_rjs :insert, :top, "test2"}
+ end
+
+ def test_elect_with_xml_namespace_attributes
+ render_html %Q{<link xlink:href="http://nowhere.com"></link>}
+ assert_nothing_raised { assert_select "link[xlink:href=http://nowhere.com]" }
end
#
@@ -331,7 +336,7 @@ class AssertSelectTest < ActionController::TestCase
# Test that we fail if there is nothing to pick.
def test_assert_select_rjs_fails_if_nothing_to_pick
render_rjs { }
- assert_raises(Assertion) { assert_select_rjs }
+ assert_raise(Assertion) { assert_select_rjs }
end
def test_assert_select_rjs_with_unicode
@@ -346,10 +351,10 @@ class AssertSelectTest < ActionController::TestCase
if str.respond_to?(:force_encoding)
str.force_encoding(Encoding::UTF_8)
assert_select str, /\343\203\201..\343\203\210/u
- assert_raises(Assertion) { assert_select str, /\343\203\201.\343\203\210/u }
+ assert_raise(Assertion) { assert_select str, /\343\203\201.\343\203\210/u }
else
assert_select str, Regexp.new("\343\203\201..\343\203\210",0,'U')
- assert_raises(Assertion) { assert_select str, Regexp.new("\343\203\201.\343\203\210",0,'U') }
+ assert_raise(Assertion) { assert_select str, Regexp.new("\343\203\201.\343\203\210",0,'U') }
end
end
end
@@ -373,7 +378,7 @@ class AssertSelectTest < ActionController::TestCase
assert_select "div", 1
assert_select "#3"
end
- assert_raises(Assertion) { assert_select_rjs "test4" }
+ assert_raise(Assertion) { assert_select_rjs "test4" }
end
def test_assert_select_rjs_for_replace
@@ -391,7 +396,7 @@ class AssertSelectTest < ActionController::TestCase
assert_select "div", 1
assert_select "#1"
end
- assert_raises(Assertion) { assert_select_rjs :replace, "test2" }
+ assert_raise(Assertion) { assert_select_rjs :replace, "test2" }
# Replace HTML.
assert_select_rjs :replace_html do
assert_select "div", 1
@@ -401,7 +406,7 @@ class AssertSelectTest < ActionController::TestCase
assert_select "div", 1
assert_select "#2"
end
- assert_raises(Assertion) { assert_select_rjs :replace_html, "test1" }
+ assert_raise(Assertion) { assert_select_rjs :replace_html, "test1" }
end
def test_assert_select_rjs_for_chained_replace
@@ -419,7 +424,7 @@ class AssertSelectTest < ActionController::TestCase
assert_select "div", 1
assert_select "#1"
end
- assert_raises(Assertion) { assert_select_rjs :chained_replace, "test2" }
+ assert_raise(Assertion) { assert_select_rjs :chained_replace, "test2" }
# Replace HTML.
assert_select_rjs :chained_replace_html do
assert_select "div", 1
@@ -429,7 +434,7 @@ class AssertSelectTest < ActionController::TestCase
assert_select "div", 1
assert_select "#2"
end
- assert_raises(Assertion) { assert_select_rjs :replace_html, "test1" }
+ assert_raise(Assertion) { assert_select_rjs :replace_html, "test1" }
end
# Simple remove
@@ -575,7 +580,7 @@ class AssertSelectTest < ActionController::TestCase
assert_select "div", 1
assert_select "#3"
end
- assert_raises(Assertion) { assert_select_rjs :insert_html, "test1" }
+ assert_raise(Assertion) { assert_select_rjs :insert_html, "test1" }
end
# Positioned insert.
@@ -608,8 +613,8 @@ class AssertSelectTest < ActionController::TestCase
end
def test_assert_select_rjs_raise_errors
- assert_raises(ArgumentError) { assert_select_rjs(:destroy) }
- assert_raises(ArgumentError) { assert_select_rjs(:insert, :left) }
+ assert_raise(ArgumentError) { assert_select_rjs(:destroy) }
+ assert_raise(ArgumentError) { assert_select_rjs(:insert, :left) }
end
# Simple selection from a single result.
@@ -701,7 +706,7 @@ EOF
#
def test_assert_select_email
- assert_raises(Assertion) { assert_select_email {} }
+ assert_raise(Assertion) { assert_select_email {} }
AssertSelectMailer.deliver_test "<div><p>foo</p><p>bar</p></div>"
assert_select_email do
assert_select "div:root" do
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
index 9af1ccc740..86dafd9221 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -428,6 +428,20 @@ class ActionCacheTest < ActionController::TestCase
assert_equal 'application/xml', @response.content_type
end
+ def test_correct_content_type_is_returned_for_cache_hit_on_action_with_string_key
+ # run it twice to cache it the first time
+ get :show, :format => 'xml'
+ get :show, :format => 'xml'
+ assert_equal 'application/xml', @response.content_type
+ end
+
+ def test_correct_content_type_is_returned_for_cache_hit_on_action_with_string_key_from_proc
+ # run it twice to cache it the first time
+ get :edit, :id => 1, :format => 'xml'
+ get :edit, :id => 1, :format => 'xml'
+ assert_equal 'application/xml', @response.content_type
+ end
+
def test_empty_path_is_normalized
@mock_controller.mock_url_for = 'http://example.org/'
@mock_controller.mock_path = '/'
diff --git a/actionpack/test/controller/fake_models.rb b/actionpack/test/controller/fake_models.rb
index 7420579ed8..0b30c79b10 100644
--- a/actionpack/test/controller/fake_models.rb
+++ b/actionpack/test/controller/fake_models.rb
@@ -9,3 +9,11 @@ end
class GoodCustomer < Customer
end
+
+module Quiz
+ class Question < Struct.new(:name, :id)
+ def to_param
+ id.to_s
+ end
+ end
+end
diff --git a/actionpack/test/controller/html-scanner/document_test.rb b/actionpack/test/controller/html-scanner/document_test.rb
index 1c3facb9e3..c68f04fa75 100644
--- a/actionpack/test/controller/html-scanner/document_test.rb
+++ b/actionpack/test/controller/html-scanner/document_test.rb
@@ -134,7 +134,7 @@ HTML
end
def test_invalid_document_raises_exception_when_strict
- assert_raises RuntimeError do
+ assert_raise RuntimeError do
doc = HTML::Document.new("<html>
<table>
<tr>
diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb
index 4913e7633b..00789eea38 100644
--- a/actionpack/test/controller/http_digest_authentication_test.rb
+++ b/actionpack/test/controller/http_digest_authentication_test.rb
@@ -5,7 +5,8 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
before_filter :authenticate, :only => :index
before_filter :authenticate_with_request, :only => :display
- USERS = { 'lifo' => 'world', 'pretty' => 'please' }
+ USERS = { 'lifo' => 'world', 'pretty' => 'please',
+ 'dhh' => ::Digest::MD5::hexdigest(["dhh","SuperSecret","secret"].join(":"))}
def index
render :text => "Hello Secret"
@@ -107,8 +108,42 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
assert_equal 'Definitely Maybe', @response.body
end
- test "authentication request with relative URI" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:uri => "/", :username => 'pretty', :password => 'please')
+ test "authentication request with valid credential and nil session" do
+ @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please')
+
+ # session_id = "" in functional test, but is +nil+ in real life
+ @request.session.session_id = nil
+ get :display
+
+ assert_response :success
+ assert assigns(:logged_in)
+ assert_equal 'Definitely Maybe', @response.body
+ end
+
+ test "authentication request with request-uri that doesn't match credentials digest-uri" do
+ @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please')
+ @request.env['REQUEST_URI'] = "/http_digest_authentication_test/dummy_digest/altered/uri"
+ get :display
+
+ assert_response :unauthorized
+ assert_equal "Authentication Failed", @response.body
+ end
+
+ test "authentication request with absolute uri" do
+ @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:uri => "http://test.host/http_digest_authentication_test/dummy_digest/display",
+ :username => 'pretty', :password => 'please')
+ @request.env['REQUEST_URI'] = "http://test.host/http_digest_authentication_test/dummy_digest/display"
+ get :display
+
+ assert_response :success
+ assert assigns(:logged_in)
+ assert_equal 'Definitely Maybe', @response.body
+ end
+
+ test "authentication request with password stored as ha1 digest hash" do
+ @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'dhh',
+ :password => ::Digest::MD5::hexdigest(["dhh","SuperSecret","secret"].join(":")),
+ :password_is_ha1 => true)
get :display
assert_response :success
@@ -119,18 +154,22 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
private
def encode_credentials(options)
- options.reverse_merge!(:nc => "00000001", :cnonce => "0a4f113b")
+ options.reverse_merge!(:nc => "00000001", :cnonce => "0a4f113b", :password_is_ha1 => false)
password = options.delete(:password)
- # Perform unautheticated get to retrieve digest parameters to use on subsequent request
+ # Set in /initializers/session_store.rb. Used as secret in generating nonce
+ # to prevent tampering of timestamp
+ ActionController::Base.session_options[:secret] = "session_options_secret"
+
+ # Perform unauthenticated GET to retrieve digest parameters to use on subsequent request
get :index
assert_response :unauthorized
credentials = decode_credentials(@response.headers['WWW-Authenticate'])
credentials.merge!(options)
- credentials.reverse_merge!(:uri => "http://#{@request.host}#{@request.env['REQUEST_URI']}")
- ActionController::HttpAuthentication::Digest.encode_credentials("GET", credentials, password)
+ credentials.reverse_merge!(:uri => "#{@request.env['REQUEST_URI']}")
+ ActionController::HttpAuthentication::Digest.encode_credentials("GET", credentials, password, options[:password_is_ha1])
end
def decode_credentials(header)
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index b3f40fbe95..e39a934c24 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -2,7 +2,7 @@ require 'abstract_unit'
class SessionTest < Test::Unit::TestCase
StubApp = lambda { |env|
- [200, {"Content-Type" => "text/html", "Content-Length" => "13"}, "Hello, World!"]
+ [200, {"Content-Type" => "text/html", "Content-Length" => "13"}, ["Hello, World!"]]
}
def setup
@@ -389,9 +389,9 @@ class MetalTest < ActionController::IntegrationTest
class Poller
def self.call(env)
if env["PATH_INFO"] =~ /^\/success/
- [200, {"Content-Type" => "text/plain", "Content-Length" => "12"}, "Hello World!"]
+ [200, {"Content-Type" => "text/plain", "Content-Length" => "12"}, ["Hello World!"]]
else
- [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, '']
+ [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []]
end
end
end
diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb
index 28555ee3d1..1575674e18 100644
--- a/actionpack/test/controller/layout_test.rb
+++ b/actionpack/test/controller/layout_test.rb
@@ -79,6 +79,10 @@ end
class DefaultLayoutController < LayoutTest
end
+class AbsolutePathLayoutController < LayoutTest
+ layout File.expand_path(File.expand_path(__FILE__) + '/../../fixtures/layout_tests/layouts/layout_test.rhtml')
+end
+
class HasOwnLayoutController < LayoutTest
layout 'item'
end
@@ -137,12 +141,18 @@ class LayoutSetInResponseTest < ActionController::TestCase
ensure
ActionController::Base.exempt_from_layout.delete(/\.rhtml$/)
end
-
+
def test_layout_is_picked_from_the_controller_instances_view_path
@controller = PrependsViewPathController.new
get :hello
assert_equal 'layouts/alt', @response.layout
end
+
+ def test_absolute_pathed_layout
+ @controller = AbsolutePathLayoutController.new
+ get :hello
+ assert_equal "layout_test.rhtml hello.rhtml", @response.body.strip
+ end
end
class RenderWithTemplateOptionController < LayoutTest
diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb
index dc59180a68..edd7162325 100644
--- a/actionpack/test/controller/mime_responds_test.rb
+++ b/actionpack/test/controller/mime_responds_test.rb
@@ -469,7 +469,7 @@ class MimeControllerTest < ActionController::TestCase
assert_equal '<html><div id="html_missing">Hello future from Firefox!</div></html>', @response.body
@request.accept = "text/iphone"
- assert_raises(ActionView::MissingTemplate) { get :iphone_with_html_response_type_without_layout }
+ assert_raise(ActionView::MissingTemplate) { get :iphone_with_html_response_type_without_layout }
end
end
diff --git a/actionpack/test/controller/polymorphic_routes_test.rb b/actionpack/test/controller/polymorphic_routes_test.rb
index 53295522ae..146d703619 100644
--- a/actionpack/test/controller/polymorphic_routes_test.rb
+++ b/actionpack/test/controller/polymorphic_routes_test.rb
@@ -18,6 +18,20 @@ class Tag < Article
def response_id; 1 end
end
+class Tax
+ attr_reader :id
+ def save; @id = 1 end
+ def new_record?; @id.nil? end
+ def name
+ model = self.class.name.downcase
+ @id.nil? ? "new #{model}" : "#{model} ##{@id}"
+ end
+end
+
+class Fax < Tax
+ def store_id; 1 end
+end
+
# TODO: test nested models
class Response::Nested < Response; end
@@ -27,6 +41,8 @@ class PolymorphicRoutesTest < ActiveSupport::TestCase
def setup
@article = Article.new
@response = Response.new
+ @tax = Tax.new
+ @fax = Fax.new
end
def test_with_record
@@ -205,4 +221,73 @@ class PolymorphicRoutesTest < ActiveSupport::TestCase
polymorphic_url(path)
end
end
+
+ # Tests for names where .plural.singular doesn't round-trip
+ def test_with_irregular_plural_record
+ @tax.save
+ expects(:taxis_url).with(@tax)
+ polymorphic_url(@tax)
+ end
+
+ def test_with_irregular_plural_new_record
+ expects(:taxes_url).with()
+ @tax.expects(:new_record?).returns(true)
+ polymorphic_url(@tax)
+ end
+
+ def test_with_irregular_plural_record_and_action
+ expects(:new_taxis_url).with()
+ @tax.expects(:new_record?).never
+ polymorphic_url(@tax, :action => 'new')
+ end
+
+ def test_irregular_plural_url_helper_prefixed_with_new
+ expects(:new_taxis_url).with()
+ new_polymorphic_url(@tax)
+ end
+
+ def test_irregular_plural_url_helper_prefixed_with_edit
+ @tax.save
+ expects(:edit_taxis_url).with(@tax)
+ edit_polymorphic_url(@tax)
+ end
+
+ def test_with_nested_irregular_plurals
+ @fax.save
+ expects(:taxis_faxis_url).with(@tax, @fax)
+ polymorphic_url([@tax, @fax])
+ end
+
+ def test_with_nested_unsaved_irregular_plurals
+ expects(:taxis_faxes_url).with(@tax)
+ polymorphic_url([@tax, @fax])
+ end
+
+ def test_new_with_irregular_plural_array_and_namespace
+ expects(:new_admin_taxis_url).with()
+ polymorphic_url([:admin, @tax], :action => 'new')
+ end
+
+ def test_unsaved_with_irregular_plural_array_and_namespace
+ expects(:admin_taxes_url).with()
+ polymorphic_url([:admin, @tax])
+ end
+
+ def test_nesting_with_irregular_plurals_and_array_ending_in_singleton_resource
+ expects(:taxis_faxis_url).with(@tax)
+ polymorphic_url([@tax, :faxis])
+ end
+
+ def test_with_array_containing_single_irregular_plural_object
+ @tax.save
+ expects(:taxis_url).with(@tax)
+ polymorphic_url([nil, @tax])
+ end
+
+ def test_with_array_containing_single_name_irregular_plural
+ @tax.save
+ expects(:taxes_url)
+ polymorphic_url([:taxes])
+ end
+
end
diff --git a/actionpack/test/controller/rack_test.rb b/actionpack/test/controller/rack_test.rb
index b550d3db78..89bf4fdacc 100644
--- a/actionpack/test/controller/rack_test.rb
+++ b/actionpack/test/controller/rack_test.rb
@@ -258,7 +258,7 @@ class RackResponseTest < BaseRackTest
}, headers)
parts = []
- body.each { |part| parts << part }
+ body.each { |part| parts << part.to_s }
assert_equal ["0", "1", "2", "3", "4"], parts
end
end
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
index 27cedc91d2..91e21db854 100644
--- a/actionpack/test/controller/redirect_test.rb
+++ b/actionpack/test/controller/redirect_test.rb
@@ -212,7 +212,7 @@ class RedirectTest < ActionController::TestCase
end
def test_redirect_to_back_with_no_referer
- assert_raises(ActionController::RedirectBackError) {
+ assert_raise(ActionController::RedirectBackError) {
@request.env["HTTP_REFERER"] = nil
get :redirect_to_back
}
@@ -239,7 +239,7 @@ class RedirectTest < ActionController::TestCase
end
def test_redirect_to_nil
- assert_raises(ActionController::ActionControllerError) do
+ assert_raise(ActionController::ActionControllerError) do
get :redirect_to_nil
end
end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index e131a735a9..a52931565d 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -36,6 +36,39 @@ class TestController < ActionController::Base
render :action => 'hello_world'
end
end
+
+ def conditional_hello_with_public_header
+ if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123], :public => true)
+ render :action => 'hello_world'
+ end
+ end
+
+ def conditional_hello_with_public_header_and_expires_at
+ expires_in 1.minute
+ if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123], :public => true)
+ render :action => 'hello_world'
+ end
+ end
+
+ def conditional_hello_with_expires_in
+ expires_in 1.minute
+ render :action => 'hello_world'
+ end
+
+ def conditional_hello_with_expires_in_with_public
+ expires_in 1.minute, :public => true
+ render :action => 'hello_world'
+ end
+
+ def conditional_hello_with_expires_in_with_public_with_more_keys
+ expires_in 1.minute, :public => true, 'max-stale' => 5.hours
+ render :action => 'hello_world'
+ end
+
+ def conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax
+ expires_in 1.minute, :public => true, :private => nil, 'max-stale' => 5.hours
+ render :action => 'hello_world'
+ end
def conditional_hello_with_bangs
render :action => 'hello_world'
@@ -124,6 +157,11 @@ class TestController < ActionController::Base
render :file => 'test/dot.directory/render_file_with_ivar'
end
+ def render_file_using_pathname
+ @secret = 'in the sauce'
+ render :file => Pathname.new(File.dirname(__FILE__)).join('..', 'fixtures', 'test', 'dot.directory', 'render_file_with_ivar.erb')
+ end
+
def render_file_from_template
@secret = 'in the sauce'
@path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar.erb'))
@@ -280,6 +318,10 @@ class TestController < ActionController::Base
def render_implicit_js_template_without_layout
end
+ def render_html_explicit_template_and_layout
+ render :template => 'test/render_implicit_html_template_from_xhr_request', :layout => 'layouts/default_html'
+ end
+
def formatted_html_erb
end
@@ -645,6 +687,14 @@ class TestController < ActionController::Base
render :partial => "hash_object", :object => {:first_name => "Sam"}
end
+ def partial_with_nested_object
+ render :partial => "quiz/questions/question", :object => Quiz::Question.new("first")
+ end
+
+ def partial_with_nested_object_shorthand
+ render Quiz::Question.new("first")
+ end
+
def partial_hash_collection
render :partial => "hash_object", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ]
end
@@ -684,12 +734,15 @@ class TestController < ActionController::Base
"render_with_explicit_string_template",
"render_js_with_explicit_template",
"render_js_with_explicit_action_template",
- "delete_with_js", "update_page", "update_page_with_instance_variables",
- "render_implicit_js_template_without_layout"
+ "delete_with_js", "update_page", "update_page_with_instance_variables"
"layouts/standard"
+ when "render_implicit_js_template_without_layout"
+ "layouts/default_html"
when "action_talk_to_layout", "layout_overriding_layout"
"layouts/talk_from_action"
+ when "render_implicit_html_template_from_xhr_request"
+ (request.xhr? ? 'layouts/xhr' : 'layouts/standard')
end
end
end
@@ -783,6 +836,11 @@ class RenderTest < ActionController::TestCase
assert_equal "<html>hello world, I'm here!</html>", @response.body
end
+ def test_xhr_with_render_text_and_layout
+ xhr :get, :render_text_hello_world_with_layout
+ assert_equal "<html>hello world, I'm here!</html>", @response.body
+ end
+
def test_do_with_render_action_and_layout_false
get :hello_world_with_layout_false
assert_equal 'Hello world!', @response.body
@@ -808,6 +866,11 @@ class RenderTest < ActionController::TestCase
assert_equal "The secret is in the sauce\n", @response.body
end
+ def test_render_file_using_pathname
+ get :render_file_using_pathname
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
def test_render_file_with_locals
get :render_file_with_locals
assert_equal "The secret is in the sauce\n", @response.body
@@ -884,11 +947,11 @@ class RenderTest < ActionController::TestCase
end
def test_attempt_to_access_object_method
- assert_raises(ActionController::UnknownAction, "No action responded to [clone]") { get :clone }
+ assert_raise(ActionController::UnknownAction, "No action responded to [clone]") { get :clone }
end
def test_private_methods
- assert_raises(ActionController::UnknownAction, "No action responded to [determine_layout]") { get :determine_layout }
+ assert_raise(ActionController::UnknownAction, "No action responded to [determine_layout]") { get :determine_layout }
end
def test_access_to_request_in_view
@@ -1018,8 +1081,13 @@ class RenderTest < ActionController::TestCase
end
def test_should_implicitly_render_html_template_from_xhr_request
- get :render_implicit_html_template_from_xhr_request, :format => :js
- assert_equal "Hello HTML!", @response.body
+ xhr :get, :render_implicit_html_template_from_xhr_request
+ assert_equal "XHR!\nHello HTML!", @response.body
+ end
+
+ def test_should_render_explicit_html_template_with_html_layout
+ xhr :get, :render_html_explicit_template_and_layout
+ assert_equal "<html>Hello HTML!</html>\n", @response.body
end
def test_should_implicitly_render_js_template_without_layout
@@ -1115,7 +1183,7 @@ class RenderTest < ActionController::TestCase
end
def test_bad_render_to_string_still_throws_exception
- assert_raises(ActionView::MissingTemplate) { get :render_to_string_with_exception }
+ assert_raise(ActionView::MissingTemplate) { get :render_to_string_with_exception }
end
def test_render_to_string_that_throws_caught_exception_doesnt_break_assigns
@@ -1140,15 +1208,15 @@ class RenderTest < ActionController::TestCase
end
def test_double_render
- assert_raises(ActionController::DoubleRenderError) { get :double_render }
+ assert_raise(ActionController::DoubleRenderError) { get :double_render }
end
def test_double_redirect
- assert_raises(ActionController::DoubleRenderError) { get :double_redirect }
+ assert_raise(ActionController::DoubleRenderError) { get :double_redirect }
end
def test_render_and_redirect
- assert_raises(ActionController::DoubleRenderError) { get :render_and_redirect }
+ assert_raise(ActionController::DoubleRenderError) { get :render_and_redirect }
end
# specify the one exception to double render rule - render_to_string followed by render
@@ -1429,6 +1497,16 @@ class RenderTest < ActionController::TestCase
assert_equal "Sam\nmaS\n", @response.body
end
+ def test_partial_with_nested_object
+ get :partial_with_nested_object
+ assert_equal "first", @response.body
+ end
+
+ def test_partial_with_nested_object_shorthand
+ get :partial_with_nested_object_shorthand
+ assert_equal "first", @response.body
+ end
+
def test_hash_partial_collection
get :partial_hash_collection
assert_equal "Pratik\nkitarP\nAmy\nymA\n", @response.body
@@ -1447,7 +1525,7 @@ class RenderTest < ActionController::TestCase
end
def test_render_missing_partial_template
- assert_raises(ActionView::MissingTemplate) do
+ assert_raise(ActionView::MissingTemplate) do
get :missing_partial
end
end
@@ -1463,6 +1541,35 @@ class RenderTest < ActionController::TestCase
end
end
+class ExpiresInRenderTest < ActionController::TestCase
+ tests TestController
+
+ def setup
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_expires_in_header
+ get :conditional_hello_with_expires_in
+ assert_equal "max-age=60, private", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_in_header_with_public
+ get :conditional_hello_with_expires_in_with_public
+ assert_equal "max-age=60, public", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_in_header_with_additional_headers
+ get :conditional_hello_with_expires_in_with_public_with_more_keys
+ assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_in_old_syntax
+ get :conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax
+ assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"]
+ end
+end
+
+
class EtagRenderTest < ActionController::TestCase
tests TestController
@@ -1552,6 +1659,16 @@ class EtagRenderTest < ActionController::TestCase
get :conditional_hello_with_bangs
assert_response :not_modified
end
+
+ def test_etag_with_public_true_should_set_header
+ get :conditional_hello_with_public_header
+ assert_equal "public", @response.headers['Cache-Control']
+ end
+
+ def test_etag_with_public_true_should_set_header_and_retain_other_headers
+ get :conditional_hello_with_public_header_and_expires_at
+ assert_equal "max-age=60, public", @response.headers['Cache-Control']
+ end
protected
def etag_for(text)
diff --git a/actionpack/test/controller/request/multipart_params_parsing_test.rb b/actionpack/test/controller/request/multipart_params_parsing_test.rb
index 054519d0d2..b812072ef4 100644
--- a/actionpack/test/controller/request/multipart_params_parsing_test.rb
+++ b/actionpack/test/controller/request/multipart_params_parsing_test.rb
@@ -103,7 +103,7 @@ class MultipartParamsParsingTest < ActionController::IntegrationTest
test "does not create tempfile if no file has been selected" do
params = parse_multipart('none')
- assert_equal %w(files submit-name), params.keys.sort
+ assert_equal %w(submit-name), params.keys.sort
assert_equal 'Larry', params['submit-name']
assert_equal nil, params['files']
end
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index ef0bf5fd08..835e73e3ab 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -79,17 +79,17 @@ module RequestForgeryProtectionTests
def test_should_not_allow_html_post_without_token
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
- assert_raises(ActionController::InvalidAuthenticityToken) { post :index, :format => :html }
+ assert_raise(ActionController::InvalidAuthenticityToken) { post :index, :format => :html }
end
def test_should_not_allow_html_put_without_token
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
- assert_raises(ActionController::InvalidAuthenticityToken) { put :index, :format => :html }
+ assert_raise(ActionController::InvalidAuthenticityToken) { put :index, :format => :html }
end
def test_should_not_allow_html_delete_without_token
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
- assert_raises(ActionController::InvalidAuthenticityToken) { delete :index, :format => :html }
+ assert_raise(ActionController::InvalidAuthenticityToken) { delete :index, :format => :html }
end
def test_should_allow_api_formatted_post_without_token
@@ -111,42 +111,42 @@ module RequestForgeryProtectionTests
end
def test_should_not_allow_api_formatted_post_sent_as_url_encoded_form_without_token
- assert_raises(ActionController::InvalidAuthenticityToken) do
+ assert_raise(ActionController::InvalidAuthenticityToken) do
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
post :index, :format => 'xml'
end
end
def test_should_not_allow_api_formatted_put_sent_as_url_encoded_form_without_token
- assert_raises(ActionController::InvalidAuthenticityToken) do
+ assert_raise(ActionController::InvalidAuthenticityToken) do
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
put :index, :format => 'xml'
end
end
def test_should_not_allow_api_formatted_delete_sent_as_url_encoded_form_without_token
- assert_raises(ActionController::InvalidAuthenticityToken) do
+ assert_raise(ActionController::InvalidAuthenticityToken) do
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
delete :index, :format => 'xml'
end
end
def test_should_not_allow_api_formatted_post_sent_as_multipart_form_without_token
- assert_raises(ActionController::InvalidAuthenticityToken) do
+ assert_raise(ActionController::InvalidAuthenticityToken) do
@request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
post :index, :format => 'xml'
end
end
def test_should_not_allow_api_formatted_put_sent_as_multipart_form_without_token
- assert_raises(ActionController::InvalidAuthenticityToken) do
+ assert_raise(ActionController::InvalidAuthenticityToken) do
@request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
put :index, :format => 'xml'
end
end
def test_should_not_allow_api_formatted_delete_sent_as_multipart_form_without_token
- assert_raises(ActionController::InvalidAuthenticityToken) do
+ assert_raise(ActionController::InvalidAuthenticityToken) do
@request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
delete :index, :format => 'xml'
end
diff --git a/actionpack/test/controller/request_test.rb b/actionpack/test/controller/request_test.rb
index efe4f136f5..c72f885a05 100644
--- a/actionpack/test/controller/request_test.rb
+++ b/actionpack/test/controller/request_test.rb
@@ -59,7 +59,7 @@ class RequestTest < ActiveSupport::TestCase
assert_equal '3.4.5.6', @request.remote_ip
@request.env['HTTP_CLIENT_IP'] = '8.8.8.8'
- e = assert_raises(ActionController::ActionControllerError) {
+ e = assert_raise(ActionController::ActionControllerError) {
@request.remote_ip
}
assert_match /IP spoofing attack/, e.message
@@ -297,7 +297,7 @@ class RequestTest < ActiveSupport::TestCase
end
def test_invalid_http_method_raises_exception
- assert_raises(ActionController::UnknownHttpMethod) do
+ assert_raise(ActionController::UnknownHttpMethod) do
self.request_method = :random_method
@request.request_method
end
@@ -311,7 +311,7 @@ class RequestTest < ActiveSupport::TestCase
end
def test_invalid_method_hacking_on_post_raises_exception
- assert_raises(ActionController::UnknownHttpMethod) do
+ assert_raise(ActionController::UnknownHttpMethod) do
self.request_method = :_random_method
@request.request_method
end
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index ae2639d245..c807e71cd7 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -99,7 +99,7 @@ class ResourcesTest < ActionController::TestCase
expected_options = {:controller => 'messages', :action => 'show', :id => '1.1.1'}
with_restful_routing :messages do
- assert_raises(ActionController::RoutingError) do
+ assert_raise(ActionController::RoutingError) do
assert_recognizes(expected_options, :path => 'messages/1.1.1', :method => :get)
end
end
@@ -175,6 +175,24 @@ class ResourcesTest < ActionController::TestCase
end
end
+ def test_with_collection_actions_and_name_prefix_and_member_action_with_same_name
+ actions = { 'a' => :get }
+
+ with_restful_routing :messages, :path_prefix => '/threads/:thread_id', :name_prefix => "thread_", :collection => actions, :member => actions do
+ assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
+ actions.each do |action, method|
+ assert_recognizes(options.merge(:action => action), :path => "/threads/1/messages/#{action}", :method => method)
+ end
+ end
+
+ assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
+ actions.keys.each do |action|
+ assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", :action => action
+ end
+ end
+ end
+ end
+
def test_with_collection_action_and_name_prefix_and_formatted
actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete }
@@ -209,6 +227,14 @@ class ResourcesTest < ActionController::TestCase
end
end
+ def test_with_member_action_and_requirement
+ expected_options = {:controller => 'messages', :action => 'mark', :id => '1.1.1'}
+
+ with_restful_routing(:messages, :requirements => {:id => /[0-9]\.[0-9]\.[0-9]/}, :member => { :mark => :get }) do
+ assert_recognizes(expected_options, :path => 'messages/1.1.1/mark', :method => :get)
+ end
+ end
+
def test_member_when_override_paths_for_default_restful_actions_with
[:put, :post].each do |method|
with_restful_routing :messages, :member => { :mark => method }, :path_names => {:new => 'nuevo'} do
@@ -325,7 +351,7 @@ class ResourcesTest < ActionController::TestCase
with_restful_routing :messages do
assert_restful_routes_for :messages do |options|
assert_recognizes(options.merge(:action => "new"), :path => "/messages/new", :method => :get)
- assert_raises(ActionController::MethodNotAllowed) do
+ assert_raise(ActionController::MethodNotAllowed) do
ActionController::Routing::Routes.recognize_path("/messages/new", :method => :post)
end
end
@@ -406,6 +432,34 @@ class ResourcesTest < ActionController::TestCase
end
end
+ def test_shallow_nested_restful_routes_with_namespaces
+ with_routing do |set|
+ set.draw do |map|
+ map.namespace :backoffice do |map|
+ map.namespace :admin do |map|
+ map.resources :products, :shallow => true do |map|
+ map.resources :images
+ end
+ end
+ end
+ end
+
+ assert_simply_restful_for :products,
+ :controller => 'backoffice/admin/products',
+ :namespace => 'backoffice/admin/',
+ :name_prefix => 'backoffice_admin_',
+ :path_prefix => 'backoffice/admin/',
+ :shallow => true
+ assert_simply_restful_for :images,
+ :controller => 'backoffice/admin/images',
+ :namespace => 'backoffice/admin/',
+ :name_prefix => 'backoffice_admin_product_',
+ :path_prefix => 'backoffice/admin/products/1/',
+ :shallow => true,
+ :options => { :product_id => '1' }
+ end
+ end
+
def test_restful_routes_dont_generate_duplicates
with_restful_routing :messages do
routes = ActionController::Routing::Routes.routes
@@ -583,11 +637,11 @@ class ResourcesTest < ActionController::TestCase
options = { :controller => controller_name.to_s }
collection_path = "/#{controller_name}"
- assert_raises(ActionController::MethodNotAllowed) do
+ assert_raise(ActionController::MethodNotAllowed) do
assert_recognizes(options.merge(:action => 'update'), :path => collection_path, :method => :put)
end
- assert_raises(ActionController::MethodNotAllowed) do
+ assert_raise(ActionController::MethodNotAllowed) do
assert_recognizes(options.merge(:action => 'destroy'), :path => collection_path, :method => :delete)
end
end
@@ -596,7 +650,7 @@ class ResourcesTest < ActionController::TestCase
def test_should_not_allow_invalid_head_method_for_member_routes
with_routing do |set|
set.draw do |map|
- assert_raises(ArgumentError) do
+ assert_raise(ArgumentError) do
map.resources :messages, :member => {:something => :head}
end
end
@@ -606,7 +660,7 @@ class ResourcesTest < ActionController::TestCase
def test_should_not_allow_invalid_http_methods_for_member_routes
with_routing do |set|
set.draw do |map|
- assert_raises(ArgumentError) do
+ assert_raise(ArgumentError) do
map.resources :messages, :member => {:something => :invalid}
end
end
@@ -750,9 +804,17 @@ class ResourcesTest < ActionController::TestCase
end
def test_with_path_segment
- with_restful_routing :messages, :as => 'reviews' do
- assert_simply_restful_for :messages, :as => 'reviews'
+ with_restful_routing :messages do
+ assert_simply_restful_for :messages
+ assert_recognizes({:controller => "messages", :action => "index"}, "/messages")
+ assert_recognizes({:controller => "messages", :action => "index"}, "/messages/")
end
+
+ with_restful_routing :messages, :as => 'reviews' do
+ assert_simply_restful_for :messages, :as => 'reviews'
+ assert_recognizes({:controller => "messages", :action => "index"}, "/reviews")
+ assert_recognizes({:controller => "messages", :action => "index"}, "/reviews/")
+ end
end
def test_multiple_with_path_segment_and_controller
@@ -1066,7 +1128,7 @@ class ResourcesTest < ActionController::TestCase
path = "#{options[:as] || controller_name}"
collection_path = "/#{options[:path_prefix]}#{path}"
- shallow_path = "/#{options[:path_prefix] unless options[:shallow]}#{path}"
+ shallow_path = "/#{options[:shallow] ? options[:namespace] : options[:path_prefix]}#{path}"
member_path = "#{shallow_path}/1"
new_path = "#{collection_path}/#{new_action}"
edit_member_path = "#{member_path}/#{edit_action}"
@@ -1130,10 +1192,10 @@ class ResourcesTest < ActionController::TestCase
options[:options].delete :action
path = "#{options[:as] || controller_name}"
- shallow_path = "/#{options[:path_prefix] unless options[:shallow]}#{path}"
+ shallow_path = "/#{options[:shallow] ? options[:namespace] : options[:path_prefix]}#{path}"
full_path = "/#{options[:path_prefix]}#{path}"
name_prefix = options[:name_prefix]
- shallow_prefix = "#{options[:name_prefix] unless options[:shallow]}"
+ shallow_prefix = options[:shallow] ? options[:namespace].try(:gsub, /\//, '_') : options[:name_prefix]
new_action = "new"
edit_action = "edit"
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index 13ba0c30dd..ef56119751 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -219,7 +219,7 @@ class DynamicSegmentTest < Test::Unit::TestCase
a_value = nil
# Local jump because of return inside eval.
- assert_raises(LocalJumpError) { eval(segment.extraction_code) }
+ assert_raise(LocalJumpError) { eval(segment.extraction_code) }
end
def test_extraction_code_should_return_on_mismatch
@@ -229,7 +229,7 @@ class DynamicSegmentTest < Test::Unit::TestCase
a_value = nil
# Local jump because of return inside eval.
- assert_raises(LocalJumpError) { eval(segment.extraction_code) }
+ assert_raise(LocalJumpError) { eval(segment.extraction_code) }
end
def test_extraction_code_should_accept_value_and_set_local
@@ -494,7 +494,7 @@ class RouteBuilderTest < Test::Unit::TestCase
defaults = {:action => 'buy', :person => nil, :car => nil}
requirements = {:person => /\w+/, :car => /^\w+$/}
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
route_requirements = builder.assign_route_options(segments, defaults, requirements)
end
@@ -882,7 +882,7 @@ class LegacyRouteSetTests < Test::Unit::TestCase
end
assert_equal({:controller => "admin/accounts", :action => "index"}, rs.recognize_path("/admin/accounts"))
assert_equal({:controller => "admin/users", :action => "index"}, rs.recognize_path("/admin/users"))
- assert_raises(ActionController::RoutingError) { rs.recognize_path("/admin/products") }
+ assert_raise(ActionController::RoutingError) { rs.recognize_path("/admin/products") }
end
def test_route_with_regexp_and_dot
@@ -955,6 +955,13 @@ class LegacyRouteSetTests < Test::Unit::TestCase
x.send(:page_url))
end
+ def test_named_route_with_blank_path_prefix
+ rs.add_named_route :page, 'page', :controller => 'content', :action => 'show_page', :path_prefix => ''
+ x = setup_for_named_route
+ assert_equal("http://test.host/page",
+ x.send(:page_url))
+ end
+
def test_named_route_with_nested_controller
rs.add_named_route :users, 'admin/user', :controller => 'admin/user', :action => 'index'
x = setup_for_named_route
@@ -1060,11 +1067,11 @@ class LegacyRouteSetTests < Test::Unit::TestCase
rs.draw do |map|
map.connect ':controller/:action/:id'
end
- assert_raises(ActionController::RoutingError) { rs.recognize_path("/not_a/show/10") }
+ assert_raise(ActionController::RoutingError) { rs.recognize_path("/not_a/show/10") }
end
def test_paths_do_not_accept_defaults
- assert_raises(ActionController::RoutingError) do
+ assert_raise(ActionController::RoutingError) do
rs.draw do |map|
map.path 'file/*path', :controller => 'content', :action => 'show_file', :path => %w(fake default)
map.connect ':controller/:action/:id'
@@ -1197,7 +1204,7 @@ class LegacyRouteSetTests < Test::Unit::TestCase
assert_equal '/post/10', rs.generate(:controller => 'post', :action => 'show', :id => 10)
- assert_raises ActionController::RoutingError do
+ assert_raise ActionController::RoutingError do
rs.generate(:controller => 'post', :action => 'show')
end
end
@@ -1407,7 +1414,7 @@ class LegacyRouteSetTests < Test::Unit::TestCase
end
x = setup_for_named_route
- assert_raises(ActionController::RoutingError) do
+ assert_raise(ActionController::RoutingError) do
x.send(:foo_with_requirement_url, "I am Against the requirements")
end
end
@@ -1539,7 +1546,7 @@ class RouteTest < Test::Unit::TestCase
end
def test_builder_complains_without_controller
- assert_raises(ArgumentError) do
+ assert_raise(ArgumentError) do
ROUTING::RouteBuilder.new.build '/contact', :contoller => "contact", :action => "index"
end
end
@@ -1822,27 +1829,27 @@ class RouteSetTest < Test::Unit::TestCase
end
def test_route_requirements_with_anchor_chars_are_invalid
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
set.draw do |map|
map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /^\d+/
end
end
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
set.draw do |map|
map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\A\d+/
end
end
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
set.draw do |map|
map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+$/
end
end
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
set.draw do |map|
map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+\Z/
end
end
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
set.draw do |map|
map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+\z/
end
@@ -1851,22 +1858,30 @@ class RouteSetTest < Test::Unit::TestCase
set.draw do |map|
map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+/, :name => /^(david|jamis)/
end
- assert_raises ActionController::RoutingError do
+ assert_raise ActionController::RoutingError do
set.generate :controller => 'pages', :action => 'show', :id => 10
end
end
end
def test_route_requirements_with_invalid_http_method_is_invalid
- assert_raises ArgumentError do
+ assert_raise 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_options_method_condition_is_valid
+ assert_nothing_raised do
+ set.draw do |map|
+ map.connect 'valid/route', :controller => 'pages', :action => 'show', :conditions => {:method => :options}
+ end
+ end
+ end
+
def test_route_requirements_with_head_method_condition_is_invalid
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
set.draw do |map|
map.connect 'valid/route', :controller => 'pages', :action => 'show', :conditions => {:method => :head}
end
@@ -1878,10 +1893,10 @@ class RouteSetTest < Test::Unit::TestCase
map.connect 'page/37s', :controller => 'pages', :action => 'show', :name => /(jamis|david)/
end
assert_equal '/page/37s', set.generate(:controller => 'pages', :action => 'show', :name => 'jamis')
- assert_raises ActionController::RoutingError do
+ assert_raise ActionController::RoutingError do
set.generate(:controller => 'pages', :action => 'show', :name => 'not_jamis')
end
- assert_raises ActionController::RoutingError do
+ assert_raise ActionController::RoutingError do
set.generate(:controller => 'pages', :action => 'show', :name => 'nor_jamis_and_david')
end
end
@@ -1924,7 +1939,7 @@ class RouteSetTest < Test::Unit::TestCase
assert_equal("update", request.path_parameters[:action])
request.recycle!
- assert_raises(ActionController::UnknownHttpMethod) {
+ assert_raise(ActionController::UnknownHttpMethod) {
request.env["REQUEST_METHOD"] = "BACON"
set.recognize(request)
}
@@ -2122,11 +2137,9 @@ class RouteSetTest < Test::Unit::TestCase
Object.const_set(:Api, Module.new { |m| m.const_set(:ProductsController, Class.new) })
set.draw do |map|
-
map.namespace 'api', :path_prefix => 'prefix' do |api|
api.route 'inventory', :controller => "products", :action => 'inventory'
end
-
end
request.path = "/prefix/inventory"
@@ -2138,6 +2151,24 @@ class RouteSetTest < Test::Unit::TestCase
Object.send(:remove_const, :Api)
end
+ def test_namespace_with_blank_path_prefix
+ Object.const_set(:Api, Module.new { |m| m.const_set(:ProductsController, Class.new) })
+
+ set.draw do |map|
+ map.namespace 'api', :path_prefix => '' do |api|
+ api.route 'inventory', :controller => "products", :action => 'inventory'
+ end
+ end
+
+ request.path = "/inventory"
+ request.env["REQUEST_METHOD"] = "GET"
+ assert_nothing_raised { set.recognize(request) }
+ assert_equal("api/products", request.path_parameters[:controller])
+ assert_equal("inventory", request.path_parameters[:action])
+ ensure
+ Object.send(:remove_const, :Api)
+ end
+
def test_generate_finds_best_fit
set.draw do |map|
map.connect "/people", :controller => "people", :action => "index"
@@ -2202,6 +2233,13 @@ class RouteSetTest < Test::Unit::TestCase
assert_equal "/my/foo/bar/7?x=y", set.generate(args)
end
+ def test_generate_with_blank_path_prefix
+ set.draw { |map| map.connect ':controller/:action/:id', :path_prefix => '' }
+
+ args = { :controller => "foo", :action => "bar", :id => "7", :x => "y" }
+ assert_equal "/foo/bar/7?x=y", set.generate(args)
+ end
+
def test_named_routes_are_never_relative_to_modules
set.draw do |map|
map.connect "/connection/manage/:action", :controller => 'connection/manage'
@@ -2237,6 +2275,22 @@ class RouteSetTest < Test::Unit::TestCase
)
end
+ def test_format_is_not_inherit
+ set.draw do |map|
+ map.connect '/posts.:format', :controller => 'posts'
+ end
+
+ assert_equal '/posts', set.generate(
+ {:controller => 'posts'},
+ {:controller => 'posts', :action => 'index', :format => 'xml'}
+ )
+
+ assert_equal '/posts.xml', set.generate(
+ {:controller => 'posts', :format => 'xml'},
+ {:controller => 'posts', :action => 'index', :format => 'xml'}
+ )
+ end
+
def test_expiry_determination_should_consider_values_with_to_param
set.draw { |map| map.connect 'projects/:project_id/:controller/:action' }
assert_equal '/projects/1/post/show', set.generate(
@@ -2293,7 +2347,7 @@ class RouteSetTest < Test::Unit::TestCase
end
def test_route_requirements_with_unsupported_regexp_options_must_error
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
set.draw do |map|
map.connect 'page/:name', :controller => 'pages',
:action => 'show',
@@ -2331,7 +2385,7 @@ class RouteSetTest < Test::Unit::TestCase
:requirements => {:name => /(david|jamis)/i}
end
assert_equal({:controller => 'pages', :action => 'show', :name => 'jamis'}, set.recognize_path('/page/jamis'))
- assert_raises ActionController::RoutingError do
+ assert_raise ActionController::RoutingError do
set.recognize_path('/page/davidjamis')
end
assert_equal({:controller => 'pages', :action => 'show', :name => 'DAVID'}, set.recognize_path('/page/DAVID'))
@@ -2345,7 +2399,7 @@ class RouteSetTest < Test::Unit::TestCase
end
url = set.generate({:controller => 'pages', :action => 'show', :name => 'david'})
assert_equal "/page/david", url
- assert_raises ActionController::RoutingError do
+ assert_raise ActionController::RoutingError do
url = set.generate({:controller => 'pages', :action => 'show', :name => 'davidjamis'})
end
url = set.generate({:controller => 'pages', :action => 'show', :name => 'JAMIS'})
@@ -2365,10 +2419,10 @@ class RouteSetTest < Test::Unit::TestCase
end
assert_equal({:controller => 'pages', :action => 'show', :name => 'jamis'}, set.recognize_path('/page/jamis'))
assert_equal({:controller => 'pages', :action => 'show', :name => 'david'}, set.recognize_path('/page/david'))
- assert_raises ActionController::RoutingError do
+ assert_raise ActionController::RoutingError do
set.recognize_path('/page/david #The Creator')
end
- assert_raises ActionController::RoutingError do
+ assert_raise ActionController::RoutingError do
set.recognize_path('/page/David')
end
end
@@ -2386,10 +2440,10 @@ class RouteSetTest < Test::Unit::TestCase
end
url = set.generate({:controller => 'pages', :action => 'show', :name => 'david'})
assert_equal "/page/david", url
- assert_raises ActionController::RoutingError do
+ assert_raise ActionController::RoutingError do
url = set.generate({:controller => 'pages', :action => 'show', :name => 'davidjamis'})
end
- assert_raises ActionController::RoutingError do
+ assert_raise ActionController::RoutingError do
url = set.generate({:controller => 'pages', :action => 'show', :name => 'JAMIS'})
end
end
diff --git a/actionpack/test/controller/selector_test.rb b/actionpack/test/controller/selector_test.rb
index 4e31b4573c..9d0613d1e2 100644
--- a/actionpack/test/controller/selector_test.rb
+++ b/actionpack/test/controller/selector_test.rb
@@ -303,7 +303,7 @@ class SelectorTest < Test::Unit::TestCase
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
# Before first and past last returns nothing.:
- assert_raises(ArgumentError) { select("tr:nth-child(-1)") }
+ assert_raise(ArgumentError) { select("tr:nth-child(-1)") }
select("tr:nth-child(0)")
assert_equal 0, @matches.size
select("tr:nth-child(5)")
@@ -597,8 +597,8 @@ class SelectorTest < Test::Unit::TestCase
def test_negation_details
parse(%Q{<p id="1"></p><p id="2"></p><p id="3"></p>})
- assert_raises(ArgumentError) { select(":not(") }
- assert_raises(ArgumentError) { select(":not(:not())") }
+ assert_raise(ArgumentError) { select(":not(") }
+ assert_raise(ArgumentError) { select(":not(:not())") }
select("p:not(#1):not(#3)")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
index 5fc79baa44..3d1904fee9 100644
--- a/actionpack/test/controller/send_file_test.rb
+++ b/actionpack/test/controller/send_file_test.rb
@@ -44,12 +44,12 @@ class SendFileTest < ActionController::TestCase
response = nil
assert_nothing_raised { response = process('file') }
assert_not_nil response
- assert_kind_of Proc, response.body
+ assert_kind_of Proc, response.body_parts
require 'stringio'
output = StringIO.new
output.binmode
- assert_nothing_raised { response.body.call(response, output) }
+ assert_nothing_raised { response.body_parts.call(response, output) }
assert_equal file_data, output.string
end
@@ -142,7 +142,7 @@ class SendFileTest < ActionController::TestCase
}
@controller.headers = {}
- assert_raises(ArgumentError){ @controller.send(:send_file_headers!, options) }
+ assert_raise(ArgumentError){ @controller.send(:send_file_headers!, options) }
end
%w(file data).each do |method|
diff --git a/actionpack/test/controller/session/cookie_store_test.rb b/actionpack/test/controller/session/cookie_store_test.rb
index 9c93ca6539..48a961ca34 100644
--- a/actionpack/test/controller/session/cookie_store_test.rb
+++ b/actionpack/test/controller/session/cookie_store_test.rb
@@ -199,29 +199,18 @@ class CookieStoreTest < ActionController::IntegrationTest
with_test_route_set do
# First request accesses the session
- time = Time.local(2008, 4, 24)
- Time.stubs(:now).returns(time)
- expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d-%b-%Y %H:%M:%S GMT")
-
cookies[SessionKey] = SignedBar
get '/set_session_value'
assert_response :success
+ cookie = headers['Set-Cookie']
- cookie_body = response.body
- assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly",
- headers['Set-Cookie']
-
- # Second request does not access the session
- time = Time.local(2008, 4, 25)
- Time.stubs(:now).returns(time)
- expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d-%b-%Y %H:%M:%S GMT")
-
+ # Second request does not access the session so the
+ # expires header should not be changed
get '/no_session_access'
assert_response :success
-
- assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly",
- headers['Set-Cookie']
+ assert_equal cookie, headers['Set-Cookie'],
+ "#{unmarshal_session(cookie).inspect} expected but was #{unmarshal_session(headers['Set-Cookie']).inspect}"
end
end
@@ -236,4 +225,13 @@ class CookieStoreTest < ActionController::IntegrationTest
yield
end
end
+
+ def unmarshal_session(cookie_string)
+ session = Rack::Utils.parse_query(cookie_string, ';,').inject({}) {|h,(k,v)|
+ h[k] = Array === v ? v.first : v
+ h
+ }[SessionKey]
+ verifier = ActiveSupport::MessageVerifier.new(SessionSecret, 'SHA1')
+ verifier.verify(session)
+ end
end
diff --git a/actionpack/test/controller/session/mem_cache_store_test.rb b/actionpack/test/controller/session/mem_cache_store_test.rb
index c3a6c8ce45..2f80a3c7c2 100644
--- a/actionpack/test/controller/session/mem_cache_store_test.rb
+++ b/actionpack/test/controller/session/mem_cache_store_test.rb
@@ -17,11 +17,14 @@ class MemCacheStoreTest < ActionController::IntegrationTest
end
def get_session_id
- render :text => "foo: #{session[:foo].inspect}; id: #{request.session_options[:id]}"
+ session[:foo]
+ render :text => "#{request.session_options[:id]}"
end
def call_reset_session
+ session[:bar]
reset_session
+ session[:bar] = "baz"
head :ok
end
@@ -58,47 +61,52 @@ class MemCacheStoreTest < ActionController::IntegrationTest
end
end
- def test_getting_session_id
+ def test_setting_session_value_after_session_reset
with_test_route_set do
get '/set_session_value'
assert_response :success
assert cookies['_session_id']
session_id = cookies['_session_id']
- get '/get_session_id'
+ get '/call_reset_session'
assert_response :success
- assert_equal "foo: \"bar\"; id: #{session_id}", response.body
- end
- end
+ assert_not_equal [], headers['Set-Cookie']
- def test_prevents_session_fixation
- with_test_route_set do
get '/get_session_value'
assert_response :success
assert_equal 'foo: nil', response.body
- session_id = cookies['_session_id']
-
- reset!
- get '/set_session_value', :_session_id => session_id
+ get '/get_session_id'
assert_response :success
- assert_equal nil, cookies['_session_id']
+ assert_not_equal session_id, response.body
end
end
- def test_setting_session_value_after_session_reset
+ def test_getting_session_id
with_test_route_set do
get '/set_session_value'
assert_response :success
assert cookies['_session_id']
+ session_id = cookies['_session_id']
- get '/call_reset_session'
+ get '/get_session_id'
assert_response :success
- assert_not_equal [], headers['Set-Cookie']
+ assert_equal session_id, response.body
+ end
+ end
+ def test_prevents_session_fixation
+ with_test_route_set do
get '/get_session_value'
assert_response :success
assert_equal 'foo: nil', response.body
+ session_id = cookies['_session_id']
+
+ reset!
+
+ get '/set_session_value', :_session_id => session_id
+ assert_response :success
+ assert_equal nil, cookies['_session_id']
end
end
rescue LoadError, RuntimeError
diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb
index 09a8356fec..863f8414c5 100644
--- a/actionpack/test/controller/url_rewriter_test.rb
+++ b/actionpack/test/controller/url_rewriter_test.rb
@@ -99,7 +99,7 @@ class UrlWriterTests < ActionController::TestCase
end
def test_exception_is_thrown_without_host
- assert_raises RuntimeError do
+ assert_raise RuntimeError do
W.new.url_for :controller => 'c', :action => 'a', :id => 'i'
end
end
diff --git a/actionpack/test/fixtures/layouts/default_html.html.erb b/actionpack/test/fixtures/layouts/default_html.html.erb
new file mode 100644
index 0000000000..edd719111c
--- /dev/null
+++ b/actionpack/test/fixtures/layouts/default_html.html.erb
@@ -0,0 +1 @@
+<html><%= @content_for_layout %></html>
diff --git a/actionpack/test/fixtures/layouts/xhr.html.erb b/actionpack/test/fixtures/layouts/xhr.html.erb
new file mode 100644
index 0000000000..85285324ec
--- /dev/null
+++ b/actionpack/test/fixtures/layouts/xhr.html.erb
@@ -0,0 +1,2 @@
+XHR!
+<%= yield %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/quiz/questions/_question.html.erb b/actionpack/test/fixtures/quiz/questions/_question.html.erb
new file mode 100644
index 0000000000..fb4dcfee64
--- /dev/null
+++ b/actionpack/test/fixtures/quiz/questions/_question.html.erb
@@ -0,0 +1 @@
+<%= question.name %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/malformed/malformed.en.html.erb~ b/actionpack/test/fixtures/test/malformed/malformed.en.html.erb~
new file mode 100644
index 0000000000..d009950384
--- /dev/null
+++ b/actionpack/test/fixtures/test/malformed/malformed.en.html.erb~
@@ -0,0 +1 @@
+Don't render me! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/malformed/malformed.erb~ b/actionpack/test/fixtures/test/malformed/malformed.erb~
new file mode 100644
index 0000000000..d009950384
--- /dev/null
+++ b/actionpack/test/fixtures/test/malformed/malformed.erb~
@@ -0,0 +1 @@
+Don't render me! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/malformed/malformed.html.erb~ b/actionpack/test/fixtures/test/malformed/malformed.html.erb~
new file mode 100644
index 0000000000..d009950384
--- /dev/null
+++ b/actionpack/test/fixtures/test/malformed/malformed.html.erb~
@@ -0,0 +1 @@
+Don't render me! \ No newline at end of file
diff --git a/actionpack/test/template/active_record_helper_test.rb b/actionpack/test/template/active_record_helper_test.rb
index e46f95d18b..83c028b5f2 100644
--- a/actionpack/test/template/active_record_helper_test.rb
+++ b/actionpack/test/template/active_record_helper_test.rb
@@ -19,6 +19,30 @@ class ActiveRecordHelperTest < ActionView::TestCase
Column = Struct.new("Column", :type, :name, :human_name)
end
+ class DirtyPost
+ class Errors
+ def empty?
+ false
+ end
+
+ def count
+ 1
+ end
+
+ def full_messages
+ ["Author name can't be <em>empty</em>"]
+ end
+
+ def on(field)
+ "can't be <em>empty</em>"
+ end
+ end
+
+ def errors
+ Errors.new
+ end
+ end
+
def setup_post
@post = Post.new
def @post.errors
@@ -195,10 +219,20 @@ class ActiveRecordHelperTest < ActionView::TestCase
assert_equal %(<div class="errorDeathByClass"><h1>1 error prohibited this post from being saved</h1><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post", :class => "errorDeathByClass", :id => nil, :header_tag => "h1")
end
+ def test_error_messages_for_escapes_html
+ @dirty_post = DirtyPost.new
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this dirty post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be &lt;em&gt;empty&lt;/em&gt;</li></ul></div>), error_messages_for("dirty_post")
+ end
+
def test_error_messages_for_handles_nil
assert_equal "", error_messages_for("notthere")
end
+ def test_error_message_on_escapes_html
+ @dirty_post = DirtyPost.new
+ assert_dom_equal "<div class=\"formError\">can't be &lt;em&gt;empty&lt;/em&gt;</div>", error_message_on(:dirty_post, :author_name)
+ end
+
def test_error_message_on_handles_nil
assert_equal "", error_message_on("notthere", "notthere")
end
diff --git a/actionpack/test/template/body_parts_test.rb b/actionpack/test/template/body_parts_test.rb
new file mode 100644
index 0000000000..4c82b75cdc
--- /dev/null
+++ b/actionpack/test/template/body_parts_test.rb
@@ -0,0 +1,22 @@
+require 'abstract_unit'
+
+class BodyPartsTest < ActionController::TestCase
+ RENDERINGS = [Object.new, Object.new, Object.new]
+
+ class TestController < ActionController::Base
+ def index
+ RENDERINGS.each do |rendering|
+ response.template.punctuate_body! rendering
+ end
+ @performed_render = true
+ end
+ end
+
+ tests TestController
+
+ def test_body_parts
+ get :index
+ assert_equal RENDERINGS, @response.body_parts
+ assert_equal RENDERINGS.join, @response.body
+ end
+end
diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb
index 5cc81b4afb..654eee40a3 100644
--- a/actionpack/test/template/form_helper_test.rb
+++ b/actionpack/test/template/form_helper_test.rb
@@ -1001,6 +1001,47 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_form_for_with_labelled_builder_with_nested_fields_for_without_options_hash
+ klass = nil
+
+ form_for(:post, @post, :builder => LabelledFormBuilder) do |f|
+ f.fields_for(:comments, Comment.new) do |nested_fields|
+ klass = nested_fields.class
+ ''
+ end
+ end
+
+ assert_equal LabelledFormBuilder, klass
+ end
+
+ def test_form_for_with_labelled_builder_with_nested_fields_for_with_options_hash
+ klass = nil
+
+ form_for(:post, @post, :builder => LabelledFormBuilder) do |f|
+ f.fields_for(:comments, Comment.new, :index => 'foo') do |nested_fields|
+ klass = nested_fields.class
+ ''
+ end
+ end
+
+ assert_equal LabelledFormBuilder, klass
+ end
+
+ class LabelledFormBuilderSubclass < LabelledFormBuilder; end
+
+ def test_form_for_with_labelled_builder_with_nested_fields_for_with_custom_builder
+ klass = nil
+
+ form_for(:post, @post, :builder => LabelledFormBuilder) do |f|
+ f.fields_for(:comments, Comment.new, :builder => LabelledFormBuilderSubclass) do |nested_fields|
+ klass = nested_fields.class
+ ''
+ end
+ end
+
+ assert_equal LabelledFormBuilderSubclass, klass
+ end
+
def test_form_for_with_html_options_adds_options_to_form_tag
form_for(:post, @post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end
expected = "<form action=\"http://www.example.com\" class=\"some_class\" id=\"some_form\" method=\"post\"></form>"
diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb
index 0c8af60aa4..c713b8da8e 100644
--- a/actionpack/test/template/form_tag_helper_test.rb
+++ b/actionpack/test/template/form_tag_helper_test.rb
@@ -266,11 +266,18 @@ class FormTagHelperTest < ActionView::TestCase
def test_submit_tag_with_confirmation
assert_dom_equal(
- %(<input name='commit' type='submit' value='Save' onclick="return confirm('Are you sure?');"/>),
+ %(<input name='commit' type='submit' value='Save' onclick="if (!confirm('Are you sure?')) return false; return true;"/>),
submit_tag("Save", :confirm => "Are you sure?")
)
end
-
+
+ def test_submit_tag_with_confirmation_and_with_disable_with
+ assert_dom_equal(
+ %(<input name="commit" type="submit" value="Save" onclick="if (!confirm('Are you sure?')) return false; if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }else { hiddenCommit = this.cloneNode(false);hiddenCommit.setAttribute('type', 'hidden');this.form.appendChild(hiddenCommit); }this.setAttribute('originalValue', this.value);this.disabled = true;this.value='Saving...';result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;" />),
+ submit_tag("Save", :disable_with => "Saving...", :confirm => "Are you sure?")
+ )
+ end
+
def test_image_submit_tag_with_confirmation
assert_dom_equal(
%(<input type="image" src="/images/save.gif" onclick="return confirm('Are you sure?');"/>),
diff --git a/actionpack/test/template/javascript_helper_test.rb b/actionpack/test/template/javascript_helper_test.rb
index d41111127b..d2fb24e36e 100644
--- a/actionpack/test/template/javascript_helper_test.rb
+++ b/actionpack/test/template/javascript_helper_test.rb
@@ -45,11 +45,6 @@ class JavaScriptHelperTest < ActionView::TestCase
link_to_function("Greeting", "alert('Hello world!')", :href => 'http://example.com/')
end
- def test_link_to_function_with_href
- assert_dom_equal %(<a href="http://example.com/" onclick="alert('Hello world!'); return false;">Greeting</a>),
- link_to_function("Greeting", "alert('Hello world!')", :href => 'http://example.com/')
- end
-
def test_button_to_function
assert_dom_equal %(<input type="button" onclick="alert('Hello world!');" value="Greeting" />),
button_to_function("Greeting", "alert('Hello world!')")
diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb
index 9c9f54936c..29cb60fd73 100644
--- a/actionpack/test/template/number_helper_test.rb
+++ b/actionpack/test/template/number_helper_test.rb
@@ -4,6 +4,7 @@ class NumberHelperTest < ActionView::TestCase
tests ActionView::Helpers::NumberHelper
def test_number_to_phone
+ assert_equal("555-1234", number_to_phone(5551234))
assert_equal("800-555-1212", number_to_phone(8005551212))
assert_equal("(800) 555-1212", number_to_phone(8005551212, {:area_code => true}))
assert_equal("800 555 1212", number_to_phone(8005551212, {:delimiter => " "}))
diff --git a/actionpack/test/template/output_buffer_test.rb b/actionpack/test/template/output_buffer_test.rb
new file mode 100644
index 0000000000..6d8eab63dc
--- /dev/null
+++ b/actionpack/test/template/output_buffer_test.rb
@@ -0,0 +1,35 @@
+require 'abstract_unit'
+
+class OutputBufferTest < ActionController::TestCase
+ class TestController < ActionController::Base
+ def index
+ render :text => 'foo'
+ end
+ end
+
+ tests TestController
+
+ def test_flush_output_buffer
+ # Start with the default body parts
+ get :index
+ assert_equal ['foo'], @response.body_parts
+ assert_nil @response.template.output_buffer
+
+ # Nil output buffer is skipped
+ @response.template.flush_output_buffer
+ assert_nil @response.template.output_buffer
+ assert_equal ['foo'], @response.body_parts
+
+ # Empty output buffer is skipped
+ @response.template.output_buffer = ''
+ @response.template.flush_output_buffer
+ assert_equal '', @response.template.output_buffer
+ assert_equal ['foo'], @response.body_parts
+
+ # Flushing appends the output buffer to the body parts
+ @response.template.output_buffer = 'bar'
+ @response.template.flush_output_buffer
+ assert_equal '', @response.template.output_buffer
+ assert_equal ['foo', 'bar'], @response.body_parts
+ end
+end
diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb
index 107c625e32..9adf053b09 100644
--- a/actionpack/test/template/render_test.rb
+++ b/actionpack/test/template/render_test.rb
@@ -145,6 +145,10 @@ module RenderTestCases
assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
end
+ def test_render_object
+ assert_equal "Hello: david", @view.render(:partial => "test/customer", :object => Customer.new("david"))
+ end
+
def test_render_partial_collection
assert_equal "Hello: davidHello: mary", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ])
end
@@ -224,6 +228,16 @@ module RenderTestCases
assert_equal 'source: Hello, <%= name %>!; locals: {:name=>"Josh"}', @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }, :type => :foo)
end
+ def test_render_ignores_templates_with_malformed_template_handlers
+ %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name|
+ assert_raise(ActionView::MissingTemplate) { @view.render(:file => "test/malformed/#{name}") }
+ end
+ end
+
+ def test_template_with_malformed_template_handler_is_reachable_through_its_exact_filename
+ assert_equal "Don't render me!", @view.render(:file => 'test/malformed/malformed.html.erb~')
+ end
+
def test_render_with_layout
assert_equal %(<title></title>\nHello world!\n),
@view.render(:file => "test/hello_world.erb", :layout => "layouts/yield")
diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb
index 564845779f..a370f1458f 100644
--- a/actionpack/test/template/text_helper_test.rb
+++ b/actionpack/test/template/text_helper_test.rb
@@ -375,6 +375,12 @@ class TextHelperTest < ActionView::TestCase
assert_equal "{link: #{link3_result}}", auto_link("{link: #{link3_raw}}")
end
+ def test_auto_link_in_tags
+ link_raw = 'http://www.rubyonrails.org/images/rails.png'
+ link_result = %Q(<img src="#{link_raw}" />)
+ assert_equal link_result, auto_link(link_result)
+ end
+
def test_auto_link_at_eol
url1 = "http://api.rubyonrails.com/Foo.html"
url2 = "http://www.ruby-doc.org/core/Bar.html"
diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb
index e7799fb204..5900709d81 100644
--- a/actionpack/test/template/url_helper_test.rb
+++ b/actionpack/test/template/url_helper_test.rb
@@ -220,7 +220,7 @@ class UrlHelperTest < ActionView::TestCase
end
def test_link_tag_using_post_javascript_and_popup
- assert_raises(ActionView::ActionViewError) { link_to("Hello", "http://www.example.com", :popup => true, :method => :post, :confirm => "Are you serious?") }
+ assert_raise(ActionView::ActionViewError) { link_to("Hello", "http://www.example.com", :popup => true, :method => :post, :confirm => "Are you serious?") }
end
def test_link_tag_using_block_in_erb
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index 9b4cb64307..f288810d90 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -4,7 +4,7 @@ module ActiveModel
# Validates whether the value of the specified attribute is available in a particular enumerable object.
#
# class Person < ActiveRecord::Base
- # validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
+ # validates_inclusion_of :gender, :in => %w( m f )
# validates_inclusion_of :age, :in => 0..99
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %s is not included in the list"
# end
diff --git a/activemodel/test/state_machine/event_test.rb b/activemodel/test/state_machine/event_test.rb
index 8fb7e82ec2..64dc8c4875 100644
--- a/activemodel/test/state_machine/event_test.rb
+++ b/activemodel/test/state_machine/event_test.rb
@@ -31,7 +31,7 @@ class EventBeingFiredTest < ActiveModel::TestCase
test 'should raise an AASM::InvalidTransition error if the transitions are empty' do
event = ActiveModel::StateMachine::Event.new(nil, :event)
- assert_raises ActiveModel::StateMachine::InvalidTransition do
+ assert_raise ActiveModel::StateMachine::InvalidTransition do
event.fire(nil)
end
end
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index fc6986912c..c73ac4649e 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,12 +1,9 @@
-*Edge*
+*2.3.2 [Final] (March 15, 2009)*
-* Added ActiveRecord::Base.each and ActiveRecord::Base.find_in_batches for batch processing [DHH/Jamis Buck]
+* Added ActiveRecord::Base.find_each and ActiveRecord::Base.find_in_batches for batch processing [DHH/Jamis Buck]
* Added that ActiveRecord::Base.exists? can be called with no arguments #1817 [Scott Taylor]
-
-*2.3.0 [RC1] (February 1st, 2009)*
-
* Add Support for updating deeply nested models from a single form. #1202 [Eloy Duran]
class Book < ActiveRecord::Base
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 8b3b97bac4..b50008c971 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -177,7 +177,7 @@ spec = Gem::Specification.new do |s|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end
- s.add_dependency('activesupport', '= 2.3.0' + PKG_BUILD)
+ s.add_dependency('activesupport', '= 2.3.2' + PKG_BUILD)
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index e2dc883b1b..6d25b36aea 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -22,7 +22,7 @@ module ActiveRecord
through_reflection = reflection.through_reflection
source_reflection_names = reflection.source_reflection_names
source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
- super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '}?")
+ super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
end
end
@@ -51,6 +51,12 @@ module ActiveRecord
end
end
+ class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
+ def initialize(reflection)
+ super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
+ end
+ end
+
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
@@ -65,7 +71,7 @@ module ActiveRecord
# See ActiveRecord::Associations::ClassMethods for documentation.
module Associations # :nodoc:
- # These classes will be loaded when associatoins are created.
+ # These classes will be loaded when associations are created.
# So there is no need to eager load them.
autoload :AssociationCollection, 'active_record/associations/association_collection'
autoload :AssociationProxy, 'active_record/associations/association_proxy'
@@ -786,11 +792,7 @@ module ActiveRecord
# 'ORDER BY p.first_name'
def has_many(association_id, options = {}, &extension)
reflection = create_has_many_reflection(association_id, options, &extension)
-
configure_dependency_for_has_many(reflection)
-
- add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
- add_multiple_associated_save_callbacks(reflection.name)
add_association_callbacks(reflection.name, reflection.options)
if options[:through]
@@ -872,10 +874,10 @@ module ActiveRecord
# [: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>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
# [: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+.
+ # association is a polymorphic +belongs_to+.
# [:readonly]
# If true, the associated object is readonly through the association.
# [:validate]
@@ -898,22 +900,9 @@ module ActiveRecord
association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation)
else
reflection = create_has_one_reflection(association_id, options)
-
- method_name = "has_one_after_save_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = association_instance_get(reflection.name)
- if association && (new_record? || association.new_record? || association[reflection.primary_key_name] != id)
- association[reflection.primary_key_name] = id
- association.save(true)
- end
- end
- after_save method_name
-
- add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
association_accessor_methods(reflection, HasOneAssociation)
association_constructor_method(:build, reflection, HasOneAssociation)
association_constructor_method(:create, reflection, HasOneAssociation)
-
configure_dependency_for_has_one(reflection)
end
end
@@ -1006,47 +995,15 @@ module ActiveRecord
if reflection.options[:polymorphic]
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
-
- method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = association_instance_get(reflection.name)
- if association && association.target
- if association.new_record?
- association.save(true)
- end
-
- if association.updated?
- self[reflection.primary_key_name] = association.id
- self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
- end
- end
- end
- before_save method_name
else
association_accessor_methods(reflection, BelongsToAssociation)
association_constructor_method(:build, reflection, BelongsToAssociation)
association_constructor_method(:create, reflection, BelongsToAssociation)
-
- method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym
- define_method(method_name) do
- if association = association_instance_get(reflection.name)
- if association.new_record?
- association.save(true)
- end
-
- if association.updated?
- self[reflection.primary_key_name] = association.id
- end
- end
- end
- before_save method_name
end
# Create the callbacks to update counter cache
if options[:counter_cache]
- cache_column = options[:counter_cache] == true ?
- "#{self.to_s.demodulize.underscore.pluralize}_count" :
- options[:counter_cache]
+ cache_column = reflection.counter_cache_column
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
define_method(method_name) do
@@ -1067,8 +1024,6 @@ module ActiveRecord
)
end
- add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
-
configure_dependency_for_belongs_to(reflection)
end
@@ -1234,9 +1189,6 @@ module ActiveRecord
# 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
def has_and_belongs_to_many(association_id, options = {}, &extension)
reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
-
- add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
- add_multiple_associated_save_callbacks(reflection.name)
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
# Don't use a before_destroy callback since users' before_destroy
@@ -1358,70 +1310,6 @@ module ActiveRecord
end
end
- def add_single_associated_validation_callbacks(association_name)
- method_name = "validate_associated_records_for_#{association_name}".to_sym
- define_method(method_name) do
- if association = association_instance_get(association_name)
- errors.add association_name unless association.target.nil? || association.valid?
- end
- end
-
- validate method_name
- end
-
- def add_multiple_associated_validation_callbacks(association_name)
- method_name = "validate_associated_records_for_#{association_name}".to_sym
- define_method(method_name) do
- association = association_instance_get(association_name)
-
- if association
- if new_record?
- association
- elsif association.loaded?
- association.select { |record| record.new_record? }
- else
- association.target.select { |record| record.new_record? }
- end.each do |record|
- errors.add association_name unless record.valid?
- end
- end
- end
-
- validate method_name
- end
-
- def add_multiple_associated_save_callbacks(association_name)
- method_name = "before_save_associated_records_for_#{association_name}".to_sym
- define_method(method_name) do
- @new_record_before_save = new_record?
- true
- end
- before_save method_name
-
- method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym
- define_method(method_name) do
- association = association_instance_get(association_name)
-
- records_to_save = if @new_record_before_save
- association
- elsif association && association.loaded?
- association.select { |record| record.new_record? }
- elsif association && !association.loaded?
- association.target.select { |record| record.new_record? }
- else
- []
- end
- records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank?
-
- # reconstruct the SQL queries now that we know the owner's id
- association.send(:construct_sql) if association.respond_to?(:construct_sql)
- end
-
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create method_name
- after_update method_name
- end
-
def association_constructor_method(constructor, reflection, association_proxy_class)
define_method("#{constructor}_#{reflection.name}") do |*params|
attributees = params.first unless params.empty?
@@ -1642,6 +1530,10 @@ module ActiveRecord
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
+
+ if reflection.association_foreign_key == reflection.primary_key_name
+ raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection)
+ end
reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
@@ -1964,9 +1856,10 @@ module ActiveRecord
def construct(parent, associations, joins, row)
case associations
when Symbol, String
- while (join = joins.shift).reflection.name.to_s != associations.to_s
- raise ConfigurationError, "Not Enough Associations" if joins.empty?
- end
+ join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name }
+ raise(ConfigurationError, "No such association") if join.nil?
+
+ joins.delete(join)
construct_association(parent, join, row)
when Array
associations.each do |association|
@@ -1974,7 +1867,11 @@ module ActiveRecord
end
when Hash
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
- association = construct_association(parent, joins.shift, row)
+ join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name }
+ raise(ConfigurationError, "No such association") if join.nil?
+
+ association = construct_association(parent, join, row)
+ joins.delete(join)
construct(association, associations[name], joins, row) if association
end
else
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index 0fefec1216..3aef1b21e9 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -60,7 +60,7 @@ module ActiveRecord
@reflection.klass.find(*args)
end
end
-
+
# Fetches the first one using SQL if possible.
def first(*args)
if fetch_first_or_last_using_find?(args)
@@ -143,6 +143,8 @@ module ActiveRecord
end
# Remove all records from this association
+ #
+ # See delete for more info.
def delete_all
load_target
delete(@target)
@@ -186,7 +188,6 @@ module ActiveRecord
end
end
-
# Removes +records+ from this association calling +before_remove+ and
# +after_remove+ callbacks.
#
@@ -195,22 +196,25 @@ module ActiveRecord
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
- records = flatten_deeper(records)
- records.each { |record| raise_on_type_mismatch(record) }
-
- transaction do
- records.each { |record| callback(:before_remove, record) }
-
- old_records = records.reject {|r| r.new_record? }
+ remove_records(records) do |records, old_records|
delete_records(old_records) if old_records.any?
-
- records.each do |record|
- @target.delete(record)
- callback(:after_remove, record)
- end
+ records.each { |record| @target.delete(record) }
end
end
+ # Destroy +records+ and remove them from this association calling
+ # +before_remove+ and +after_remove+ callbacks.
+ #
+ # Note that this method will _always_ remove records from the database
+ # ignoring the +:dependent+ option.
+ def destroy(*records)
+ remove_records(records) do |records, old_records|
+ old_records.each { |record| record.destroy }
+ end
+
+ load_target
+ end
+
# Removes all records from this association. Returns +self+ so method calls may be chained.
def clear
return self if length.zero? # forces load_target if it hasn't happened already
@@ -223,15 +227,16 @@ module ActiveRecord
self
end
-
- def destroy_all
- transaction do
- each { |record| record.destroy }
- end
+ # Destory all the records from this association.
+ #
+ # See destroy for more info.
+ def destroy_all
+ load_target
+ destroy(@target)
reset_target!
end
-
+
def create(attrs = {})
if attrs.is_a?(Array)
attrs.collect { |attr| create(attr) }
@@ -431,6 +436,18 @@ module ActiveRecord
record
end
+ def remove_records(*records)
+ records = flatten_deeper(records)
+ records.each { |record| raise_on_type_mismatch(record) }
+
+ transaction do
+ records.each { |record| callback(:before_remove, record) }
+ old_records = records.reject { |r| r.new_record? }
+ yield(records, old_records)
+ records.each { |record| callback(:after_remove, record) }
+ end
+ end
+
def callback(method, record)
callbacks_for(method).each do |callback|
ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)
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 a5cc3bf091..af9ce3dfb2 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
@@ -28,12 +28,12 @@ module ActiveRecord
load_target.size
end
- def insert_record(record, force=true)
+ def insert_record(record, force = true, validate = true)
if record.new_record?
if force
record.save!
else
- return false unless record.save
+ return false unless record.save(validate)
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 3348079e9d..a2cbabfe0c 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -56,9 +56,9 @@ module ActiveRecord
"#{@reflection.name}_count"
end
- def insert_record(record)
+ def insert_record(record, force = false, validate = true)
set_belongs_to_association_for(record)
- record.save
+ force ? record.save! : record.save(validate)
end
# Deletes the records according to the <tt>:dependent</tt> option.
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 2eeeb28d1f..1c091e7d5a 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -23,8 +23,8 @@ module ActiveRecord
end
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
- # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
- # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
+ # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero,
+ # and you need to fetch that collection afterwards, it'll take one fewer SELECT query if you use #length.
def size
return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
return @target.size if loaded?
@@ -47,12 +47,12 @@ module ActiveRecord
options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil?
end
- def insert_record(record, force=true)
+ def insert_record(record, force = true, validate = true)
if record.new_record?
if force
record.save!
else
- return false unless record.save
+ return false unless record.save(validate)
end
end
through_reflection = @reflection.through_reflection
@@ -150,7 +150,7 @@ module ActiveRecord
end
else
reflection_primary_key = @reflection.source_reflection.primary_key_name
- source_primary_key = @reflection.klass.primary_key
+ source_primary_key = @reflection.through_reflection.klass.primary_key
if @reflection.source_reflection.options[:as]
polymorphic_join = "AND %s.%s = %s" % [
@reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 960323004d..b92cbbdeab 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -29,8 +29,17 @@ module ActiveRecord
unless @target.nil? || @target == obj
if dependent? && !dont_save
- @target.destroy unless @target.new_record?
- @owner.clear_association_cache
+ case @reflection.options[:dependent]
+ when :delete
+ @target.delete unless @target.new_record?
+ @owner.clear_association_cache
+ when :destroy
+ @target.destroy unless @target.new_record?
+ @owner.clear_association_cache
+ when :nullify
+ @target[@reflection.primary_key_name] = nil
+ @target.save unless @owner.new_record? || @target.new_record?
+ end
else
@target[@reflection.primary_key_name] = nil
@target.save unless @owner.new_record? || @target.new_record?
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 680b41518c..741aa2acbe 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -125,79 +125,63 @@ module ActiveRecord
# post.author.name = ''
# post.save(false) # => true
module AutosaveAssociation
+ ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }
+
def self.included(base)
base.class_eval do
+ base.extend(ClassMethods)
alias_method_chain :reload, :autosave_associations
- alias_method_chain :save, :autosave_associations
- alias_method_chain :save!, :autosave_associations
- alias_method_chain :valid?, :autosave_associations
- %w{ has_one belongs_to has_many has_and_belongs_to_many }.each do |type|
+ ASSOCIATION_TYPES.each do |type|
base.send("valid_keys_for_#{type}_association") << :autosave
end
end
end
- # Saves the parent, <tt>self</tt>, and any loaded autosave associations.
- # In addition, it destroys all children that were marked for destruction
- # with mark_for_destruction.
- #
- # This all happens inside a transaction, _if_ the Transactions module is included into
- # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
- def save_with_autosave_associations(perform_validation = true)
- returning(save_without_autosave_associations(perform_validation)) do |valid|
- if valid
- self.class.reflect_on_all_autosave_associations.each do |reflection|
- if (association = association_instance_get(reflection.name)) && association.loaded?
- if association.is_a?(Array)
- association.proxy_target.each do |child|
- child.marked_for_destruction? ? child.destroy : child.save(perform_validation)
- end
- else
- association.marked_for_destruction? ? association.destroy : association.save(perform_validation)
- end
- end
+ module ClassMethods
+ private
+
+ # def belongs_to(name, options = {})
+ # super
+ # add_autosave_association_callbacks(reflect_on_association(name))
+ # end
+ ASSOCIATION_TYPES.each do |type|
+ module_eval %{
+ def #{type}(name, options = {})
+ super
+ add_autosave_association_callbacks(reflect_on_association(name))
end
- end
+ }
end
- end
- # Attempts to save the record just like save_with_autosave_associations but
- # will raise a RecordInvalid exception instead of returning false if the
- # record is not valid.
- def save_with_autosave_associations!
- if valid_with_autosave_associations?
- save_with_autosave_associations(false) || raise(RecordNotSaved)
- else
- raise RecordInvalid.new(self)
- end
- end
+ # Adds a validate and save callback for the association as specified by
+ # the +reflection+.
+ def add_autosave_association_callbacks(reflection)
+ save_method = "autosave_associated_records_for_#{reflection.name}"
+ validation_method = "validate_associated_records_for_#{reflection.name}"
+ validate validation_method
- # Returns whether or not the parent, <tt>self</tt>, and any loaded autosave associations are valid.
- def valid_with_autosave_associations?
- if valid_without_autosave_associations?
- self.class.reflect_on_all_autosave_associations.all? do |reflection|
- if (association = association_instance_get(reflection.name)) && association.loaded?
- if association.is_a?(Array)
- association.proxy_target.all? { |child| autosave_association_valid?(reflection, child) }
- else
- autosave_association_valid?(reflection, association)
- end
- else
- true # association not loaded yet, so it should be valid
+ case reflection.macro
+ when :has_many, :has_and_belongs_to_many
+ before_save :before_save_collection_association
+
+ define_method(save_method) { save_collection_association(reflection) }
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
+ after_create save_method
+ after_update save_method
+
+ define_method(validation_method) { validate_collection_association(reflection) }
+ else
+ case reflection.macro
+ when :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ after_save save_method
+ when :belongs_to
+ define_method(save_method) { save_belongs_to_association(reflection) }
+ before_save save_method
end
+ define_method(validation_method) { validate_single_association(reflection) }
end
- else
- false # self was not valid
- end
- end
-
- # Returns whether or not the association is valid and applies any errors to the parent, <tt>self</tt>, if it wasn't.
- def autosave_association_valid?(reflection, association)
- returning(association.valid?) do |valid|
- association.errors.each do |attribute, message|
- errors.add "#{reflection.name}_#{attribute}", message
- end unless valid
end
end
@@ -221,5 +205,145 @@ module ActiveRecord
def marked_for_destruction?
@marked_for_destruction
end
+
+ private
+
+ # Returns the record for an association collection that should be validated
+ # or saved. If +autosave+ is +false+ only new records will be returned,
+ # unless the parent is/was a new record itself.
+ def associated_records_to_validate_or_save(association, new_record, autosave)
+ if new_record
+ association
+ elsif association.loaded?
+ autosave ? association : association.select { |record| record.new_record? }
+ else
+ autosave ? association.target : association.target.select { |record| record.new_record? }
+ end
+ end
+
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
+ # turned on for the association specified by +reflection+.
+ def validate_single_association(reflection)
+ if reflection.options[:validate] == true || reflection.options[:autosave] == true
+ if (association = association_instance_get(reflection.name)) && !association.target.nil?
+ association_valid?(reflection, association)
+ end
+ end
+ end
+
+ # Validate the associated records if <tt>:validate</tt> or
+ # <tt>:autosave</tt> is turned on for the association specified by
+ # +reflection+.
+ def validate_collection_association(reflection)
+ if reflection.options[:validate] != false && association = association_instance_get(reflection.name)
+ if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
+ records.each { |record| association_valid?(reflection, record) }
+ end
+ end
+ end
+
+ # Returns whether or not the association is valid and applies any errors to
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
+ # enabled records if they're marked_for_destruction?.
+ def association_valid?(reflection, association)
+ unless valid = association.valid?
+ if reflection.options[:autosave]
+ unless association.marked_for_destruction?
+ association.errors.each do |attribute, message|
+ attribute = "#{reflection.name}_#{attribute}"
+ errors.add(attribute, message) unless errors.on(attribute)
+ end
+ end
+ else
+ errors.add(reflection.name)
+ end
+ end
+ valid
+ end
+
+ # Is used as a before_save callback to check while saving a collection
+ # association whether or not the parent was a new record before saving.
+ def before_save_collection_association
+ @new_record_before_save = new_record?
+ true
+ end
+
+ # Saves any new associated records, or all loaded autosave associations if
+ # <tt>:autosave</tt> is enabled on the association.
+ #
+ # In addition, it destroys all children that were marked for destruction
+ # with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ autosave = reflection.options[:autosave]
+
+ if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+ records.each do |record|
+ if autosave && record.marked_for_destruction?
+ association.destroy(record)
+ elsif @new_record_before_save || record.new_record?
+ if autosave
+ association.send(:insert_record, record, false, false)
+ else
+ association.send(:insert_record, record)
+ end
+ elsif autosave
+ record.save(false)
+ end
+ end
+ end
+
+ # reconstruct the SQL queries now that we know the owner's id
+ association.send(:construct_sql) if association.respond_to?(:construct_sql)
+ end
+ end
+
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+ # on the association.
+ #
+ # In addition, it will destroy the association if it was marked for
+ # destruction with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_has_one_association(reflection)
+ if (association = association_instance_get(reflection.name)) && !association.target.nil?
+ if reflection.options[:autosave] && association.marked_for_destruction?
+ association.destroy
+ elsif new_record? || association.new_record? || association[reflection.primary_key_name] != id || reflection.options[:autosave]
+ association[reflection.primary_key_name] = id
+ association.save(false)
+ end
+ end
+ end
+
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+ # on the association.
+ #
+ # In addition, it will destroy the association if it was marked for
+ # destruction with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_belongs_to_association(reflection)
+ if association = association_instance_get(reflection.name)
+ if reflection.options[:autosave] && association.marked_for_destruction?
+ association.destroy
+ else
+ association.save(false) if association.new_record? || reflection.options[:autosave]
+
+ if association.updated?
+ self[reflection.primary_key_name] = association.id
+ # TODO: Removing this code doesn't seem to matter…
+ if reflection.options[:polymorphic]
+ self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
+ end
+ end
+ end
+ end
+ end
end
end \ No newline at end of file
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 55ab1facf2..9943a7014a 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -416,7 +416,7 @@ module ActiveRecord #:nodoc:
end
@@subclasses = {}
-
+
##
# :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
@@ -661,7 +661,6 @@ module ActiveRecord #:nodoc:
connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
end
-
# Returns true if a record exists in the table that matches the +id+ or
# conditions given, or false otherwise. The argument can take five forms:
#
@@ -737,12 +736,12 @@ module ActiveRecord #:nodoc:
# ==== Parameters
#
# * +id+ - This should be the id or an array of ids to be updated.
- # * +attributes+ - This should be a Hash of attributes to be set on the object, or an array of Hashes.
+ # * +attributes+ - This should be a hash of attributes to be set on the object, or an array of hashes.
#
# ==== Examples
#
# # Updating one record:
- # Person.update(15, { :user_name => 'Samuel', :group => 'expert' })
+ # Person.update(15, :user_name => 'Samuel', :group => 'expert')
#
# # Updating multiple records:
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
@@ -811,25 +810,28 @@ module ActiveRecord #:nodoc:
# Updates all records with details given if they match a set of conditions supplied, limits and order can
# also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
- # database. It does not instantiate the involved models and it does not trigger Active Record callbacks.
+ # database. It does not instantiate the involved models and it does not trigger Active Record callbacks
+ # or validations.
#
# ==== Parameters
#
- # * +updates+ - A string of column and value pairs that will be set on any records that match conditions. This creates the SET clause of the generated SQL.
- # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info.
+ # * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
+ # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. See conditions in the intro.
# * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage.
#
# ==== Examples
#
- # # Update all billing objects with the 3 different attributes given
- # Billing.update_all( "category = 'authorized', approved = 1, author = 'David'" )
+ # # Update all customers with the given attributes
+ # Customer.update_all :wants_email => true
+ #
+ # # Update all books with 'Rails' in their title
+ # Book.update_all "author = 'David'", "title LIKE '%Rails%'"
#
- # # Update records that match our conditions
- # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'" )
+ # # Update all avatars migrated more than a week ago
+ # Avatar.update_all ['migrated_at = ?, Time.now.utc], ['migrated_at > ?', 1.week.ago]
#
- # # Update records that match our conditions but limit it to 5 ordered by date
- # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'",
- # :order => 'created_at', :limit => 5 )
+ # # Update all books that match our conditions, but limit it to 5 ordered by date
+ # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5
def update_all(updates, conditions = nil, options = {})
sql = "UPDATE #{quoted_table_name} SET #{sanitize_sql_for_assignment(updates)} "
@@ -1003,7 +1005,6 @@ module ActiveRecord #:nodoc:
update_counters(id, counter_name => -1)
end
-
# Attributes named in this macro are protected from mass-assignment,
# such as <tt>new(attributes)</tt>,
# <tt>update_attributes(attributes)</tt>, or
@@ -1104,7 +1105,6 @@ module ActiveRecord #:nodoc:
read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {})
end
-
# Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
# directly from ActiveRecord::Base. So if the hierarchy looks like: Reply < Message < ActiveRecord::Base, then Message is used
# to guess the table name even when called on Reply. The rules used to do the guess are handled by the Inflector class
@@ -1347,7 +1347,7 @@ module ActiveRecord #:nodoc:
subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
end
- def self_and_descendents_from_active_record#nodoc:
+ def self_and_descendants_from_active_record#nodoc:
klass = self
classes = [klass]
while klass != klass.base_class
@@ -1367,7 +1367,7 @@ module ActiveRecord #:nodoc:
# 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|
+ defaults = self_and_descendants_from_active_record.map do |klass|
:"#{klass.name.underscore}.#{attribute_key_name}"
end
defaults << options[:default] if options[:default]
@@ -1382,7 +1382,7 @@ module ActiveRecord #:nodoc:
# 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|
+ defaults = self_and_descendants_from_active_record.map do |klass|
:"#{klass.name.underscore}"
end
defaults << self.name.humanize
@@ -1417,7 +1417,6 @@ module ActiveRecord #:nodoc:
end
end
-
def quote_value(value, column = nil) #:nodoc:
connection.quote(value,column)
end
@@ -1486,7 +1485,7 @@ module ActiveRecord #:nodoc:
elsif match = DynamicScopeMatch.match(method_id)
return true if all_attributes_exists?(match.attribute_names)
end
-
+
super
end
@@ -1537,7 +1536,7 @@ module ActiveRecord #:nodoc:
end
def reverse_sql_order(order_query)
- reversed_query = order_query.split(/,/).each { |s|
+ reversed_query = order_query.to_s.split(/,/).each { |s|
if s.match(/\s(asc|ASC)$/)
s.gsub!(/\s(asc|ASC)$/, ' DESC')
elsif s.match(/\s(desc|DESC)$/)
@@ -1690,7 +1689,7 @@ module ActiveRecord #:nodoc:
def construct_finder_sql(options)
scope = scope(:find)
sql = "SELECT #{options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))} "
- sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
+ sql << "FROM #{options[:from] || (scope && scope[:from]) || quoted_table_name} "
add_joins!(sql, options[:joins], scope)
add_conditions!(sql, options[:conditions], scope)
@@ -1745,7 +1744,9 @@ module ActiveRecord #:nodoc:
scoped_order = scope[:order] if scope
if order
sql << " ORDER BY #{order}"
- sql << ", #{scoped_order}" if scoped_order
+ if scoped_order && scoped_order != order
+ sql << ", #{scoped_order}"
+ end
else
sql << " ORDER BY #{scoped_order}" if scoped_order
end
@@ -1754,12 +1755,12 @@ module ActiveRecord #:nodoc:
def add_group!(sql, group, having, scope = :auto)
if group
sql << " GROUP BY #{group}"
- sql << " HAVING #{having}" if having
+ sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having
else
scope = scope(:find) if :auto == scope
if scope && (scoped_group = scope[:group])
sql << " GROUP BY #{scoped_group}"
- sql << " HAVING #{scoped_having}" if (scoped_having = scope[:having])
+ sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having]
end
end
end
@@ -2014,7 +2015,6 @@ module ActiveRecord #:nodoc:
end
end
-
# Defines an "attribute" method (like +inheritance_column+ or
# +table_name+). A new (class) method will be created with the
# given name. If a value is specified, the new method will
@@ -2111,7 +2111,7 @@ module ActiveRecord #:nodoc:
end
# Merge scopings
- if action == :merge && current_scoped_methods
+ if [:merge, :reverse_merge].include?(action) && current_scoped_methods
method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)|
case hash[method]
when Hash
@@ -2133,7 +2133,11 @@ module ActiveRecord #:nodoc:
end
end
else
- hash[method] = hash[method].merge(params)
+ if action == :reverse_merge
+ hash[method] = hash[method].merge(params)
+ else
+ hash[method] = params.merge(hash[method])
+ end
end
else
hash[method] = params
@@ -2143,7 +2147,6 @@ module ActiveRecord #:nodoc:
end
self.scoped_methods << method_scoping
-
begin
yield
ensure
@@ -2174,7 +2177,7 @@ module ActiveRecord #:nodoc:
# Test whether the given method and optional key are scoped.
def scoped?(method, key = nil) #:nodoc:
if current_scoped_methods && (scope = current_scoped_methods[method])
- !key || scope.has_key?(key)
+ !key || !scope[key].nil?
end
end
@@ -2749,7 +2752,6 @@ module ActiveRecord #:nodoc:
assign_multiparameter_attributes(multi_parameter_attributes)
end
-
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
def attributes
self.attribute_names.inject({}) do |attrs, name|
diff --git a/activerecord/lib/active_record/batches.rb b/activerecord/lib/active_record/batches.rb
index 9e9c8fbbf4..5a6cecd4ad 100644
--- a/activerecord/lib/active_record/batches.rb
+++ b/activerecord/lib/active_record/batches.rb
@@ -4,21 +4,24 @@ module ActiveRecord
base.extend(ClassMethods)
end
- # When processing large numbers of records, it's often a good idea to do so in batches to prevent memory ballooning.
+ # When processing large numbers of records, it's often a good idea to do
+ # so in batches to prevent memory ballooning.
module ClassMethods
- # Yields each record that was found by the find +options+. The find is performed by find_in_batches
- # with a batch size of 1000 (or as specified by the +batch_size+ option).
+ # Yields each record that was found by the find +options+. The find is
+ # performed by find_in_batches with a batch size of 1000 (or as
+ # specified by the <tt>:batch_size</tt> option).
#
# Example:
#
- # Person.each(:conditions => "age > 21") do |person|
+ # Person.find_each(:conditions => "age > 21") do |person|
# person.party_all_night!
# end
#
- # Note: This method is only intended to use for batch processing of large amounts of records that wouldn't fit in
- # memory all at once. If you just need to loop over less than 1000 records, it's probably better just to use the
- # regular find methods.
- def each(options = {})
+ # Note: This method is only intended to use for batch processing of
+ # large amounts of records that wouldn't fit in memory all at once. If
+ # you just need to loop over less than 1000 records, it's probably
+ # better just to use the regular find methods.
+ def find_each(options = {})
find_in_batches(options) do |records|
records.each { |record| yield record }
end
@@ -26,17 +29,22 @@ module ActiveRecord
self
end
- # Yields each batch of records that was found by the find +options+ as an array. The size of each batch is
- # set by the +batch_size+ option; the default is 1000.
+ # Yields each batch of records that was found by the find +options+ as
+ # an array. The size of each batch is set by the <tt>:batch_size</tt>
+ # option; the default is 1000.
#
- # You can control the starting point for the batch processing by supplying the +start+ option. This is especially
- # useful if you want multiple workers dealing with the same processing queue. You can make worker 1 handle all the
- # records between id 0 and 10,000 and worker 2 handle from 10,000 and beyond (by setting the +start+ option on that
- # worker).
+ # You can control the starting point for the batch processing by
+ # supplying the <tt>:start</tt> option. This is especially useful if you
+ # want multiple workers dealing with the same processing queue. You can
+ # make worker 1 handle all the records between id 0 and 10,000 and
+ # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt>
+ # option on that worker).
#
- # It's not possible to set the order. That is automatically set to ascending on the primary key ("id ASC")
- # to make the batch ordering work. This also mean that this method only works with integer-based primary keys.
- # You can't set the limit either, that's used to control the the batch sizes.
+ # It's not possible to set the order. That is automatically set to
+ # ascending on the primary key ("id ASC") to make the batch ordering
+ # work. This also mean that this method only works with integer-based
+ # primary keys. You can't set the limit either, that's used to control
+ # the the batch sizes.
#
# Example:
#
@@ -49,12 +57,15 @@ module ActiveRecord
raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit]
start = options.delete(:start).to_i
+ batch_size = options.delete(:batch_size) || 1000
- with_scope(:find => options.merge(:order => batch_order, :limit => options.delete(:batch_size) || 1000)) do
+ with_scope(:find => options.merge(:order => batch_order, :limit => batch_size)) do
records = find(:all, :conditions => [ "#{table_name}.#{primary_key} >= ?", start ])
while records.any?
yield records
+
+ break if records.size < batch_size
records = find(:all, :conditions => [ "#{table_name}.#{primary_key} > ?", records.last.id ])
end
end
diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb
index b239c03284..f077818d3b 100644
--- a/activerecord/lib/active_record/calculations.rb
+++ b/activerecord/lib/active_record/calculations.rb
@@ -141,22 +141,30 @@ module ActiveRecord
def construct_count_options_from_args(*args)
options = {}
column_name = :all
-
+
# We need to handle
# count()
# count(:column_name=:all)
# count(options={})
# count(column_name=:all, options={})
+ # selects specified by scopes
case args.size
+ when 0
+ column_name = scope(:find)[:select] if scope(:find)
when 1
- args[0].is_a?(Hash) ? options = args[0] : column_name = args[0]
+ if args[0].is_a?(Hash)
+ column_name = scope(:find)[:select] if scope(:find)
+ options = args[0]
+ else
+ column_name = args[0]
+ end
when 2
column_name, options = args
else
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
- end if args.size > 0
-
- [column_name, options]
+ end
+
+ [column_name || :all, options]
end
def construct_calculation_sql(operation, column_name, options) #:nodoc:
@@ -214,13 +222,15 @@ module ActiveRecord
end
if options[:group] && options[:having]
+ having = sanitize_sql_for_conditions(options[:having])
+
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
if connection.adapter_name == 'FrontBase'
- options[:having].downcase!
- options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
+ having.downcase!
+ having.gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
end
- sql << " HAVING #{options[:having]} "
+ sql << " HAVING #{having} "
end
sql << " ORDER BY #{options[:order]} " if options[:order]
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 901b17124c..aac84cc5f4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -351,5 +351,21 @@ module ActiveRecord
retrieve_connection_pool klass.superclass
end
end
+
+ class ConnectionManagement
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ @app.call(env)
+ ensure
+ # Don't return connection (and peform implicit rollback) if
+ # this request is a part of integration test
+ unless env.key?("rack.test")
+ ActiveRecord::Base.clear_active_connections!
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index cc9c46505f..75420f69aa 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -18,7 +18,7 @@ module ActiveRecord
db.busy_timeout(config[:timeout]) unless config[:timeout].nil?
- ConnectionAdapters::SQLite3Adapter.new(db, logger)
+ ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index 5390f49f04..afd6472db8 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -17,9 +17,9 @@ module ActiveRecord
# "Downgrade" deprecated sqlite API
if SQLite.const_defined?(:Version)
- ConnectionAdapters::SQLite2Adapter.new(db, logger)
+ ConnectionAdapters::SQLite2Adapter.new(db, logger, config)
else
- ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger)
+ ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger, config)
end
end
end
@@ -72,10 +72,31 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLiteAdapter < AbstractAdapter
+ class Version
+ include Comparable
+
+ def initialize(version_string)
+ @version = version_string.split('.').map(&:to_i)
+ end
+
+ def <=>(version_string)
+ @version <=> version_string.split('.').map(&:to_i)
+ end
+ end
+
+ def initialize(connection, logger, config)
+ super(connection, logger)
+ @config = config
+ end
+
def adapter_name #:nodoc:
'SQLite'
end
+ def supports_ddl_transactions?
+ sqlite_version >= '2.0.0'
+ end
+
def supports_migrations? #:nodoc:
true
end
@@ -83,6 +104,10 @@ module ActiveRecord
def requires_reloading?
true
end
+
+ def supports_add_column?
+ sqlite_version >= '3.1.6'
+ end
def disconnect!
super
@@ -164,7 +189,6 @@ module ActiveRecord
catch_schema_changes { @connection.rollback }
end
-
# SELECT ... FOR UPDATE is redundant since the table is locked.
def add_lock!(sql, options) #:nodoc:
sql
@@ -213,14 +237,20 @@ module ActiveRecord
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
end
+ # See: http://www.sqlite.org/lang_altertable.html
+ # SQLite has an additional restriction on the ALTER TABLE statement
+ def valid_alter_table_options( type, options)
+ type.to_sym != :primary_key
+ end
+
def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if @connection.respond_to?(:transaction_active?) && @connection.transaction_active?
- raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction'
+ if supports_add_column? && valid_alter_table_options( type, options )
+ super(table_name, column_name, type, options)
+ else
+ alter_table(table_name) do |definition|
+ definition.column(column_name, type, options)
+ end
end
-
- super(table_name, column_name, type, options)
- # See last paragraph on http://www.sqlite.org/lang_altertable.html
- execute "VACUUM"
end
def remove_column(table_name, *column_names) #:nodoc:
@@ -380,7 +410,7 @@ module ActiveRecord
end
def sqlite_version
- @sqlite_version ||= select_value('select sqlite_version(*)')
+ @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)'))
end
def default_primary_key_type
@@ -393,23 +423,9 @@ module ActiveRecord
end
class SQLite2Adapter < SQLiteAdapter # :nodoc:
- def supports_count_distinct? #:nodoc:
- false
- end
-
def rename_table(name, new_name)
move_table(name, new_name)
end
-
- def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if @connection.respond_to?(:transaction_active?) && @connection.transaction_active?
- raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction'
- end
-
- alter_table(table_name) do |definition|
- definition.column(column_name, type, options)
- end
- end
end
class DeprecatedSQLiteAdapter < SQLite2Adapter # :nodoc:
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index ff9899d032..7fa7e267d8 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -23,6 +23,16 @@ module ActiveRecord
# p2.first_name = "should fail"
# p2.save # Raises a ActiveRecord::StaleObjectError
#
+ # Optimistic locking will also check for stale data when objects are destroyed. Example:
+ #
+ # p1 = Person.find(1)
+ # p2 = Person.find(1)
+ #
+ # p1.first_name = "Michael"
+ # p1.save
+ #
+ # p2.destroy # Raises a ActiveRecord::StaleObjectError
+ #
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
@@ -39,6 +49,7 @@ module ActiveRecord
base.lock_optimistically = true
base.alias_method_chain :update, :lock
+ base.alias_method_chain :destroy, :lock
base.alias_method_chain :attributes_from_column_definition, :lock
class << base
@@ -98,6 +109,28 @@ module ActiveRecord
end
end
+ def destroy_with_lock #:nodoc:
+ return destroy_without_lock unless locking_enabled?
+
+ unless new_record?
+ lock_col = self.class.locking_column
+ previous_value = send(lock_col).to_i
+
+ affected_rows = connection.delete(
+ "DELETE FROM #{self.class.quoted_table_name} " +
+ "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " +
+ "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}",
+ "#{self.class.name} Destroy"
+ )
+
+ unless affected_rows == 1
+ raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object"
+ end
+ end
+
+ freeze
+ end
+
module ClassMethods
DEFAULT_LOCKING_COLUMN = 'lock_version'
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index 989b2a1ec5..1f3ef300f2 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -1,11 +1,12 @@
module ActiveRecord
module NamedScope
- # All subclasses of ActiveRecord::Base have two named \scopes:
- # * <tt>all</tt> - which is similar to a <tt>find(:all)</tt> query, and
+ # All subclasses of ActiveRecord::Base have one named scope:
# * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
#
# These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
# intermediate values (scopes) around as first-class objects is convenient.
+ #
+ # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
def self.included(base)
base.class_eval do
extend ClassMethods
@@ -88,7 +89,12 @@ module ActiveRecord
when Hash
options
when Proc
- options.call(*args)
+ case parent_scope
+ when Scope
+ with_scope(:find => parent_scope.proxy_options) { options.call(*args) }
+ else
+ options.call(*args)
+ end
end, &block)
end
(class << self; self end).instance_eval do
@@ -98,9 +104,9 @@ module ActiveRecord
end
end
end
-
+
class Scope
- attr_reader :proxy_scope, :proxy_options
+ attr_reader :proxy_scope, :proxy_options, :current_scoped_methods_when_defined
NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
[].methods.each do |m|
unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
@@ -111,8 +117,12 @@ module ActiveRecord
delegate :scopes, :with_scope, :to => :proxy_scope
def initialize(proxy_scope, options, &block)
+ options ||= {}
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
extend Module.new(&block) if block_given?
+ unless Scope === proxy_scope
+ @current_scoped_methods_when_defined = proxy_scope.send(:current_scoped_methods)
+ end
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
end
@@ -166,9 +176,15 @@ module ActiveRecord
if scopes.include?(method)
scopes[method].call(self, *args)
else
- with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} do
+ with_scope({:find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {}}, :reverse_merge) do
method = :new if method == :build
- proxy_scope.send(method, *args, &block)
+ if current_scoped_methods_when_defined
+ with_scope current_scoped_methods_when_defined do
+ proxy_scope.send(method, *args, &block)
+ end
+ else
+ proxy_scope.send(method, *args, &block)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index e69bfb1355..2d4c1d5507 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -197,7 +197,7 @@ module ActiveRecord
def counter_cache_column
if options[:counter_cache] == true
- "#{active_record.name.underscore.pluralize}_count"
+ "#{active_record.name.demodulize.underscore.pluralize}_count"
elsif options[:counter_cache]
options[:counter_cache]
end
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 4749823b94..fa75874603 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -231,16 +231,22 @@ module ActiveRecord #:nodoc:
def add_associations(association, records, opts)
if records.is_a?(Enumerable)
tag = reformat_name(association.to_s)
+ type = options[:skip_types] ? {} : {:type => "array"}
+
if records.empty?
- builder.tag!(tag, :type => :array)
+ builder.tag!(tag, type)
else
- builder.tag!(tag, :type => :array) do
+ builder.tag!(tag, type) do
association_name = association.to_s.singularize
records.each do |record|
- record.to_xml opts.merge(
- :root => association_name,
- :type => (record.class.to_s.underscore == association_name ? nil : record.class.name)
- )
+ if options[:skip_types]
+ record_type = {}
+ else
+ record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
+ record_type = {:type => record_class}
+ end
+
+ record.to_xml opts.merge(:root => association_name).merge(record_type)
end
end
end
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
index de199d30bf..3cc4640f42 100644
--- a/activerecord/lib/active_record/session_store.rb
+++ b/activerecord/lib/active_record/session_store.rb
@@ -287,8 +287,7 @@ module ActiveRecord
def get_session(env, sid)
Base.silence do
sid ||= generate_sid
- session = @@session_class.find_by_session_id(sid)
- session ||= @@session_class.new(:session_id => sid, :data => {})
+ session = find_session(sid)
env[SESSION_RECORD_KEY] = session
[sid, session.data]
end
@@ -296,7 +295,7 @@ module ActiveRecord
def set_session(env, sid, session_data)
Base.silence do
- record = env[SESSION_RECORD_KEY]
+ record = env[SESSION_RECORD_KEY] ||= find_session(sid)
record.data = session_data
return false unless record.save
@@ -310,5 +309,10 @@ module ActiveRecord
return true
end
+
+ def find_session(id)
+ @@session_class.find_by_session_id(id) ||
+ @@session_class.new(:session_id => id, :data => {})
+ end
end
end
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index 211dd78874..8c6abaaccb 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -49,5 +49,18 @@ module ActiveRecord
ActiveRecord::Base.clear_all_connections!
ActiveRecord::Base.establish_connection(@connection)
end
+
+ def with_kcode(kcode)
+ if RUBY_VERSION < '1.9'
+ orig_kcode, $KCODE = $KCODE, kcode
+ begin
+ yield
+ ensure
+ $KCODE = orig_kcode
+ end
+ else
+ yield
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 0b6e52c79b..b059eb7f6f 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -175,6 +175,8 @@ module ActiveRecord
# end # RELEASE savepoint active_record_1
# # ^^^^ BOOM! database error!
# end
+ #
+ # Note that "TRUNCATE" is also a MySQL DDL statement!
module ClassMethods
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(options = {}, &block)
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 8f3c80565e..d2d12b80c9 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -89,7 +89,7 @@ module ActiveRecord
message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
- defaults = @base.class.self_and_descendents_from_active_record.map do |klass|
+ defaults = @base.class.self_and_descendants_from_active_record.map do |klass|
[ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
:"models.#{klass.name.underscore}.#{message}" ]
end
@@ -720,20 +720,20 @@ module ActiveRecord
# class (which has a database table to query from).
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
- is_text_column = finder_class.columns_hash[attr_name.to_s].text?
+ column = finder_class.columns_hash[attr_name.to_s]
if value.nil?
comparison_operator = "IS ?"
- elsif is_text_column
+ elsif column.text?
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
- value = value.to_s
+ value = column.limit ? value.to_s[0, column.limit] : 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)
+ if value.nil? || (configuration[:case_sensitive] || !column.text?)
condition_sql = "#{sql_attribute} #{comparison_operator}"
condition_params = [value]
else
@@ -802,7 +802,7 @@ module ActiveRecord
# Validates whether the value of the specified attribute is available in a particular enumerable object.
#
# class Person < ActiveRecord::Base
- # validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
+ # validates_inclusion_of :gender, :in => %w( m f )
# validates_inclusion_of :age, :in => 0..99
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list"
# end
@@ -1040,6 +1040,11 @@ module ActiveRecord
errors.empty?
end
+ # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise.
+ def invalid?
+ !valid?
+ end
+
# Returns the Errors object that holds all information about attribute error messages.
def errors
@errors ||= Errors.new(self)
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index 6ac4bdc905..852807b4c5 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module VERSION #:nodoc:
MAJOR = 2
MINOR = 3
- TINY = 0
+ TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 40a8503980..13a78a1890 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -154,6 +154,23 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 0, Topic.find(t2.id).replies.size
end
+ def test_belongs_to_reassign_with_namespaced_models_and_counters
+ t1 = Web::Topic.create("title" => "t1")
+ t2 = Web::Topic.create("title" => "t2")
+ r1 = Web::Reply.new("title" => "r1", "content" => "r1")
+ r1.topic = t1
+
+ assert r1.save
+ assert_equal 1, Web::Topic.find(t1.id).replies.size
+ assert_equal 0, Web::Topic.find(t2.id).replies.size
+
+ r1.topic = Web::Topic.find(t2.id)
+
+ assert r1.save
+ assert_equal 0, Web::Topic.find(t1.id).replies.size
+ assert_equal 1, Web::Topic.find(t2.id).replies.size
+ end
+
def test_belongs_to_counter_after_save
topic = Topic.create!(:title => "monday night")
topic.replies.create!(:title => "re: monday night", :content => "football")
@@ -190,19 +207,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
end
- def test_assignment_before_parent_saved
- client = Client.find(:first)
- apple = Firm.new("name" => "Apple")
- client.firm = apple
- assert_equal apple, client.firm
- assert apple.new_record?
- assert client.save
- assert apple.save
- assert !apple.new_record?
- assert_equal apple, client.firm
- assert_equal apple, client.firm(true)
- end
-
def test_assignment_before_child_saved
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
@@ -215,19 +219,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal firm, final_cut.firm(true)
end
- def test_assignment_before_either_saved
- final_cut = Client.new("name" => "Final Cut")
- apple = Firm.new("name" => "Apple")
- final_cut.firm = apple
- assert final_cut.new_record?
- assert apple.new_record?
- assert final_cut.save
- assert !final_cut.new_record?
- assert !apple.new_record?
- assert_equal apple, final_cut.firm
- assert_equal apple, final_cut.firm(true)
- end
-
def test_new_record_with_foreign_key_but_no_object
c = Client.new("firm_id" => 1)
assert_equal Firm.find(:first), c.firm_with_basic_id
@@ -274,90 +265,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 17, reply.replies.size
end
- def test_store_two_association_with_one_save
- num_orders = Order.count
- num_customers = Customer.count
- order = Order.new
-
- customer1 = order.billing = Customer.new
- customer2 = order.shipping = Customer.new
- assert order.save
- assert_equal customer1, order.billing
- assert_equal customer2, order.shipping
-
- order.reload
-
- assert_equal customer1, order.billing
- assert_equal customer2, order.shipping
-
- assert_equal num_orders +1, Order.count
- assert_equal num_customers +2, Customer.count
- end
-
-
- def test_store_association_in_two_relations_with_one_save
- num_orders = Order.count
- num_customers = Customer.count
- order = Order.new
-
- customer = order.billing = order.shipping = Customer.new
- assert order.save
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- order.reload
-
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- assert_equal num_orders +1, Order.count
- assert_equal num_customers +1, Customer.count
- end
-
- def test_store_association_in_two_relations_with_one_save_in_existing_object
- num_orders = Order.count
- num_customers = Customer.count
- order = Order.create
-
- customer = order.billing = order.shipping = Customer.new
- assert order.save
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- order.reload
-
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- assert_equal num_orders +1, Order.count
- assert_equal num_customers +1, Customer.count
- end
-
- def test_store_association_in_two_relations_with_one_save_in_existing_object_with_values
- num_orders = Order.count
- num_customers = Customer.count
- order = Order.create
-
- customer = order.billing = order.shipping = Customer.new
- assert order.save
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- order.reload
-
- customer = order.billing = order.shipping = Customer.new
-
- assert order.save
- order.reload
-
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- assert_equal num_orders +1, Order.count
- assert_equal num_customers +2, Customer.count
- end
-
-
def test_association_assignment_sticks
post = Post.find(:first)
@@ -410,32 +317,29 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal nil, sponsor.sponsorable_id
end
- def test_save_fails_for_invalid_belongs_to
- assert log = AuditLog.create(:developer_id=>0,:message=>"")
-
- log.developer = Developer.new
- assert !log.developer.valid?
- assert !log.valid?
- assert !log.save
- assert_equal "is invalid", log.errors.on("developer")
- end
-
- def test_save_succeeds_for_invalid_belongs_to_with_validate_false
- assert log = AuditLog.create(:developer_id=>0,:message=>"")
-
- log.unvalidated_developer = Developer.new
- assert !log.unvalidated_developer.valid?
- assert log.valid?
- assert log.save
- end
-
def test_belongs_to_proxy_should_not_respond_to_private_methods
- assert_raises(NoMethodError) { companies(:first_firm).private_method }
- assert_raises(NoMethodError) { companies(:second_client).firm.private_method }
+ assert_raise(NoMethodError) { companies(:first_firm).private_method }
+ assert_raise(NoMethodError) { companies(:second_client).firm.private_method }
end
def test_belongs_to_proxy_should_respond_to_private_methods_via_send
companies(:first_firm).send(:private_method)
companies(:second_client).firm.send(:private_method)
end
+
+ def test_save_of_record_with_loaded_belongs_to
+ @account = companies(:first_firm).account
+
+ assert_nothing_raised do
+ Account.find(@account.id).save!
+ Account.find(@account.id, :include => :firm).save!
+ end
+
+ @account.firm.delete
+
+ assert_nothing_raised do
+ Account.find(@account.id).save!
+ Account.find(@account.id, :include => :firm).save!
+ end
+ 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 12dec5ccd1..1b2e0fc11e 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -1,4 +1,9 @@
require 'cases/helper'
+require 'models/author'
+require 'models/post'
+require 'models/comment'
+require 'models/category'
+require 'models/categorization'
module Remembered
def self.included(base)
@@ -99,3 +104,27 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
end
end
end
+
+class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase
+ def setup
+ @davey_mcdave = Author.create(:name => 'Davey McDave')
+ @first_post = @davey_mcdave.posts.create(:title => 'Davey Speaks', :body => 'Expressive wordage')
+ @first_comment = @first_post.comments.create(:body => 'Inflamatory doublespeak')
+ @first_categorization = @davey_mcdave.categorizations.create(:category => Category.first, :post => @first_post)
+ end
+
+ def teardown
+ @davey_mcdave.destroy
+ @first_post.destroy
+ @first_comment.destroy
+ @first_categorization.destroy
+ end
+
+ def test_missing_data_in_a_nested_include_should_not_cause_errors_when_constructing_objects
+ assert_nothing_raised do
+ # @davey_mcdave doesn't have any author_favorites
+ includes = {:posts => :comments, :categorizations => :category, :author_favorites => :favorite_author }
+ Author.all :include => includes, :conditions => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name'
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 14099d4176..40723814c5 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -549,16 +549,16 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_invalid_association_reference
- assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=> :monkeys )
}
- assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=>[ :monkeys ])
}
- assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=>[ 'monkeys' ])
}
- assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
+ assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
post = Post.find(6, :include=>[ :monkeys, :elephants ])
}
end
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 0f43d97f4a..5e8b2cadfc 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
@@ -381,6 +381,33 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_date_from_db Date.new(2004, 10, 10), Developer.find(1).projects.first.joined_on.to_date
end
+ def test_destroying
+ david = Developer.find(1)
+ active_record = Project.find(1)
+ david.projects.reload
+ assert_equal 2, david.projects.size
+ assert_equal 3, active_record.developers.size
+
+ assert_difference "Project.count", -1 do
+ david.projects.destroy(active_record)
+ end
+
+ assert_equal 1, david.reload.projects.size
+ assert_equal 1, david.projects(true).size
+ end
+
+ def test_destroying_array
+ david = Developer.find(1)
+ david.projects.reload
+
+ assert_difference "Project.count", -Project.count do
+ david.projects.destroy(Project.find(:all))
+ end
+
+ assert_equal 0, david.reload.projects.size
+ assert_equal 0, david.projects(true).size
+ end
+
def test_destroy_all
david = Developer.find(1)
david.projects.reload
@@ -616,7 +643,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_updating_attributes_on_rich_associations
david = projects(:action_controller).developers.first
david.name = "DHH"
- assert_raises(ActiveRecord::ReadOnlyRecord) { david.save! }
+ assert_raise(ActiveRecord::ReadOnlyRecord) { david.save! }
end
def test_updating_attributes_on_rich_associations_with_limited_find_from_reflection
@@ -740,6 +767,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developer, project.developers.find(:first)
assert_equal project, developer.projects.find(:first)
end
+
+ def test_self_referential_habtm_without_foreign_key_set_should_raise_exception
+ assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) {
+ Member.class_eval do
+ has_and_belongs_to_many :friends, :class_name => "Member", :join_table => "member_friends"
+ end
+ }
+ end
def test_dynamic_find_should_respect_association_include
# SQL error in sort clause if :include is not included
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index a2525f1d70..30edf79a26 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -70,6 +70,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, companies(:first_firm).limited_clients.find(:all, :limit => nil).size
end
+ def test_dynamic_find_last_without_specified_order
+ assert_equal companies(:second_client), companies(:first_firm).unsorted_clients.find_last_by_type('Client')
+ end
+
def test_dynamic_find_should_respect_association_order
assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find(:first, :conditions => "type = 'Client'")
assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client')
@@ -176,7 +180,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_find_ids
firm = Firm.find(:first)
- assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find }
+ assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find }
client = firm.clients.find(2)
assert_kind_of Client, client
@@ -190,7 +194,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, client_ary.size
assert_equal client, client_ary.first
- assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
+ assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
end
def test_find_string_ids_when_using_finder_sql
@@ -215,6 +219,45 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, firm.clients.find(:all, :conditions => "name = 'Summit'").length
end
+ def test_find_each
+ firm = companies(:first_firm)
+
+ assert ! firm.clients.loaded?
+
+ assert_queries(3) do
+ firm.clients.find_each(:batch_size => 1) {|c| assert_equal firm.id, c.firm_id }
+ end
+
+ assert ! firm.clients.loaded?
+ end
+
+ def test_find_each_with_conditions
+ firm = companies(:first_firm)
+
+ assert_queries(2) do
+ firm.clients.find_each(:batch_size => 1, :conditions => {:name => "Microsoft"}) do |c|
+ assert_equal firm.id, c.firm_id
+ assert_equal "Microsoft", c.name
+ end
+ end
+
+ assert ! firm.clients.loaded?
+ end
+
+ def test_find_in_batches
+ firm = companies(:first_firm)
+
+ assert ! firm.clients.loaded?
+
+ assert_queries(2) do
+ firm.clients.find_in_batches(:batch_size => 2) do |clients|
+ clients.each {|c| assert_equal firm.id, c.firm_id }
+ end
+ end
+
+ assert ! firm.clients.loaded?
+ end
+
def test_find_all_sanitized
firm = Firm.find(:first)
summit = firm.clients.find(:all, :conditions => "name = 'Summit'")
@@ -238,7 +281,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_find_in_collection
assert_equal Client.find(2).name, companies(:first_firm).clients.find(2).name
- assert_raises(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) }
+ assert_raise(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) }
end
def test_find_grouped
@@ -278,36 +321,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_create_with_bang_on_has_many_when_parent_is_new_raises
- assert_raises(ActiveRecord::RecordNotSaved) do
+ assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create! :name=>"Whoever"
end
end
def test_regular_create_on_has_many_when_parent_is_new_raises
- assert_raises(ActiveRecord::RecordNotSaved) do
+ assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create :name=>"Whoever"
end
end
def test_create_with_bang_on_has_many_raises_when_record_not_saved
- assert_raises(ActiveRecord::RecordInvalid) do
+ assert_raise(ActiveRecord::RecordInvalid) do
firm = Firm.find(:first)
firm.plain_clients.create!
end
end
def test_create_with_bang_on_habtm_when_parent_is_new_raises
- assert_raises(ActiveRecord::RecordNotSaved) do
+ assert_raise(ActiveRecord::RecordNotSaved) do
Developer.new("name" => "Aredridel").projects.create!
end
end
def test_adding_a_mismatch_class
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << nil }
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << 1 }
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << Topic.find(1) }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << nil }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << 1 }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << Topic.find(1) }
end
def test_adding_a_collection
@@ -317,81 +360,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, companies(:first_firm).clients_of_firm(true).size
end
- def test_adding_before_save
- no_of_firms = Firm.count
- no_of_clients = Client.count
-
- new_firm = Firm.new("name" => "A New Firm, Inc")
- c = Client.new("name" => "Apple")
-
- new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
- assert_equal 1, new_firm.clients_of_firm.size
- new_firm.clients_of_firm << c
- assert_equal 2, new_firm.clients_of_firm.size
-
- assert_equal no_of_firms, Firm.count # Firm was not saved to database.
- assert_equal no_of_clients, Client.count # Clients were not saved to database.
- assert new_firm.save
- assert !new_firm.new_record?
- assert !c.new_record?
- assert_equal new_firm, c.firm
- assert_equal no_of_firms+1, Firm.count # Firm was saved to database.
- assert_equal no_of_clients+2, Client.count # Clients were saved to database.
-
- assert_equal 2, new_firm.clients_of_firm.size
- assert_equal 2, new_firm.clients_of_firm(true).size
- end
-
- def test_invalid_adding
- firm = Firm.find(1)
- assert !(firm.clients_of_firm << c = Client.new)
- assert c.new_record?
- assert !firm.valid?
- assert !firm.save
- assert c.new_record?
- end
-
- def test_invalid_adding_before_save
- no_of_firms = Firm.count
- no_of_clients = Client.count
- new_firm = Firm.new("name" => "A New Firm, Inc")
- new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
- assert c.new_record?
- assert !c.valid?
- assert !new_firm.valid?
- assert !new_firm.save
- assert c.new_record?
- assert new_firm.new_record?
- end
-
- def test_invalid_adding_with_validate_false
- firm = Firm.find(:first)
- client = Client.new
- firm.unvalidated_clients_of_firm << client
-
- assert firm.valid?
- assert !client.valid?
- assert firm.save
- assert client.new_record?
- end
-
- def test_valid_adding_with_validate_false
- no_of_clients = Client.count
-
- firm = Firm.find(:first)
- client = Client.new("name" => "Apple")
-
- assert firm.valid?
- assert client.valid?
- assert client.new_record?
-
- firm.unvalidated_clients_of_firm << client
-
- assert firm.save
- assert !client.new_record?
- assert_equal no_of_clients+1, Client.count
- end
-
def test_build
company = companies(:first_firm)
new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
@@ -400,10 +368,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal "Another Client", new_client.name
assert new_client.new_record?
assert_equal new_client, company.clients_of_firm.last
- company.name += '-changed'
- assert_queries(2) { assert company.save }
- assert !new_client.new_record?
- assert_equal 2, company.clients_of_firm(true).size
end
def test_collection_size_after_building
@@ -428,11 +392,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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"}]) }
-
assert_equal 2, new_clients.size
- company.name += '-changed'
- assert_queries(3) { assert company.save }
- assert_equal 3, company.clients_of_firm(true).size
end
def test_build_followed_by_save_does_not_load_target
@@ -463,10 +423,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal "Another Client", new_client.name
assert new_client.new_record?
assert_equal new_client, company.clients_of_firm.last
- company.name += '-changed'
- assert_queries(2) { assert company.save }
- assert !new_client.new_record?
- assert_equal 2, company.clients_of_firm(true).size
end
def test_build_many_via_block
@@ -480,10 +436,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, new_clients.size
assert_equal "changed", new_clients.first.name
assert_equal "changed", new_clients.last.name
-
- company.name += '-changed'
- assert_queries(3) { assert company.save }
- assert_equal 3, company.clients_of_firm(true).size
end
def test_create_without_loading_association
@@ -501,16 +453,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, first_firm.clients_of_firm.size
end
- def test_invalid_build
- new_client = companies(:first_firm).clients_of_firm.build
- assert new_client.new_record?
- assert !new_client.valid?
- assert_equal new_client, companies(:first_firm).clients_of_firm.last
- assert !companies(:first_firm).save
- assert new_client.new_record?
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
- end
-
def test_create
force_signal37_to_load_all_clients_of_firm
new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client")
@@ -703,7 +645,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_invalid_belongs_to_dependent_option_raises_exception
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
Author.belongs_to :special_author_address, :dependent => :nullify
end
end
@@ -729,13 +671,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_type_mismatch
david = Developer.find(1)
david.projects.reload
- assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) }
end
def test_deleting_self_type_mismatch
david = Developer.find(1)
david.projects.reload
- assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) }
+ end
+
+ def test_destroying
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_difference "Client.count", -1 do
+ companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first)
+ end
+
+ assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_destroying_a_collection
+ force_signal37_to_load_all_clients_of_firm
+ companies(:first_firm).clients_of_firm.create("name" => "Another Client")
+ assert_equal 2, companies(:first_firm).clients_of_firm.size
+
+ assert_difference "Client.count", -2 do
+ companies(:first_firm).clients_of_firm.destroy([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]])
+ end
+
+ assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_destroy_all
@@ -843,15 +809,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert !firm.clients.include?(:first_client)
end
- def test_replace_on_new_object
- firm = Firm.new("name" => "New Firm")
- firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
- assert firm.save
- firm.reload
- assert_equal 2, firm.clients.length
- assert firm.clients.include?(Client.find_by_name("New Client"))
- end
-
def test_get_ids
assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids
end
@@ -879,15 +836,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert company.clients_using_sql.loaded?
end
- def test_assign_ids
- firm = Firm.new("name" => "Apple")
- firm.client_ids = [companies(:first_client).id, companies(:second_client).id]
- firm.save
- firm.reload
- assert_equal 2, firm.clients.length
- assert firm.clients.include?(companies(:second_client))
- end
-
def test_assign_ids_ignoring_blanks
firm = Firm.create!(:name => 'Apple')
firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, '']
@@ -910,16 +858,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection, &block) }
end
-
- def test_assign_ids_for_through_a_belongs_to
- post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!")
- post.person_ids = [people(:david).id, people(:michael).id]
- post.save
- post.reload
- assert_equal 2, post.people.length
- assert post.people.include?(people(:david))
- end
-
def test_dynamic_find_should_respect_association_order_for_through
assert_equal Comment.find(10), authors(:david).comments_desc.find(:first, :conditions => "comments.type = 'SpecialComment'")
assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type('SpecialComment')
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 1e5d1a0202..97efca7891 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -1,14 +1,19 @@
require "cases/helper"
require 'models/post'
require 'models/person'
+require 'models/reference'
+require 'models/job'
require 'models/reader'
require 'models/comment'
require 'models/tag'
require 'models/tagging'
require 'models/author'
+require 'models/owner'
+require 'models/pet'
+require 'models/toy'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
- fixtures :posts, :readers, :people, :comments, :authors
+ fixtures :posts, :readers, :people, :comments, :authors, :owners, :pets, :toys
def test_associate_existing
assert_queries(2) { posts(:thinking);people(:david) }
@@ -87,6 +92,24 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert posts(:welcome).reload.people(true).empty?
end
+ def test_destroy_association
+ assert_difference "Person.count", -1 do
+ posts(:welcome).people.destroy(people(:michael))
+ end
+
+ assert posts(:welcome).reload.people.empty?
+ assert posts(:welcome).people(true).empty?
+ end
+
+ def test_destroy_all
+ assert_difference "Person.count", -1 do
+ posts(:welcome).people.destroy_all
+ end
+
+ assert posts(:welcome).reload.people.empty?
+ assert posts(:welcome).people(true).empty?
+ end
+
def test_replace_association
assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
@@ -249,4 +272,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
author.author_favorites.create(:favorite_author_id => 3)
assert_equal post.author.author_favorites, post.author_favorites
end
+
+ def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys
+ assert_equal 1, owners(:blackbeard).toys.count
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 14032a67c0..1ddb3f49bf 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -59,8 +59,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_type_mismatch
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
end
def test_natural_assignment
@@ -76,7 +76,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).save
assert_nil companies(:first_firm).account
# account is dependent, therefore is destroyed when reference to owner is lost
- assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
+ assert_raise(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
+ end
+
+ def test_nullification_on_association_change
+ firm = companies(:rails_core)
+ old_account_id = firm.account.id
+ firm.account = Account.new
+ # account is dependent with nullify, therefore its firm_id should be nil
+ assert_nil Account.find(old_account_id).firm_id
+ end
+
+ def test_association_changecalls_delete
+ companies(:first_firm).deletable_account = Account.new
+ assert_equal [], Account.destroyed_account_ids[companies(:first_firm).id]
+ end
+
+ def test_association_change_calls_destroy
+ companies(:first_firm).account = Account.new
+ assert_equal [companies(:first_firm).id], Account.destroyed_account_ids[companies(:first_firm).id]
end
def test_natural_assignment_to_already_associated_record
@@ -193,28 +211,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal account, firm.account
end
- def test_build_before_child_saved
- firm = Firm.find(1)
-
- account = firm.account.build("credit_limit" => 1000)
- assert_equal account, firm.account
- assert account.new_record?
- assert firm.save
- assert_equal account, firm.account
- assert !account.new_record?
- end
-
- def test_build_before_either_saved
- firm = Firm.new("name" => "GlobalMegaCorp")
-
- firm.account = account = Account.new("credit_limit" => 1000)
- assert_equal account, firm.account
- assert account.new_record?
- assert firm.save
- assert_equal account, firm.account
- assert !account.new_record?
- end
-
def test_failing_build_association
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
@@ -253,16 +249,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
firm.destroy
end
- def test_assignment_before_parent_saved
- firm = Firm.new("name" => "GlobalMegaCorp")
- firm.account = a = Account.find(1)
- assert firm.new_record?
- assert_equal a, firm.account
- assert firm.save
- assert_equal a, firm.account
- assert_equal a, firm.account(true)
- end
-
def test_finding_with_interpolated_condition
firm = Firm.find(:first)
superior = firm.clients.create(:name => 'SuperiorCo')
@@ -279,61 +265,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal a, firm.account
assert_equal a, firm.account(true)
end
-
- def test_save_fails_for_invalid_has_one
- firm = Firm.find(:first)
- assert firm.valid?
-
- firm.account = Account.new
-
- assert !firm.account.valid?
- assert !firm.valid?
- assert !firm.save
- assert_equal "is invalid", firm.errors.on("account")
- end
-
-
- def test_save_succeeds_for_invalid_has_one_with_validate_false
- firm = Firm.find(:first)
- assert firm.valid?
-
- firm.unvalidated_account = Account.new
-
- assert !firm.unvalidated_account.valid?
- assert firm.valid?
- assert firm.save
- end
-
- def test_assignment_before_either_saved
- firm = Firm.new("name" => "GlobalMegaCorp")
- firm.account = a = Account.new("credit_limit" => 1000)
- assert firm.new_record?
- assert a.new_record?
- assert_equal a, firm.account
- assert firm.save
- assert !firm.new_record?
- assert !a.new_record?
- assert_equal a, firm.account
- assert_equal a, firm.account(true)
- end
-
- def test_not_resaved_when_unchanged
- firm = Firm.find(:first, :include => :account)
- firm.name += '-changed'
- assert_queries(1) { firm.save! }
-
- firm = Firm.find(:first)
- firm.account = Account.find(:first)
- assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! }
-
- firm = Firm.find(:first).clone
- firm.account = Account.find(:first)
- assert_queries(2) { firm.save! }
-
- firm = Firm.find(:first).clone
- firm.account = Account.find(:first).clone
- assert_queries(2) { firm.save! }
- end
def test_save_still_works_after_accessing_nil_has_one
jp = Company.new :name => 'Jaded Pixel'
@@ -350,8 +281,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_proxy_should_not_respond_to_private_methods
- assert_raises(NoMethodError) { accounts(:signals37).private_method }
- assert_raises(NoMethodError) { companies(:first_firm).account.private_method }
+ assert_raise(NoMethodError) { accounts(:signals37).private_method }
+ assert_raise(NoMethodError) { companies(:first_firm).account.private_method }
end
def test_has_one_proxy_should_respond_to_private_methods_via_send
@@ -359,4 +290,20 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).account.send(:private_method)
end
+ def test_save_of_record_with_loaded_has_one
+ @firm = companies(:first_firm)
+ assert_not_nil @firm.account
+
+ assert_nothing_raised do
+ Firm.find(@firm.id).save!
+ Firm.find(@firm.id, :include => :account).save!
+ end
+
+ @firm.account.destroy
+
+ assert_nothing_raised do
+ Firm.find(@firm.id).save!
+ Firm.find(@firm.id, :include => :account).save!
+ end
+ end
end
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 f65d76e2ce..12c598751b 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -115,8 +115,8 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_through_proxy_should_not_respond_to_private_methods
- assert_raises(NoMethodError) { clubs(:moustache_club).private_method }
- assert_raises(NoMethodError) { @member.club.private_method }
+ assert_raise(NoMethodError) { clubs(:moustache_club).private_method }
+ assert_raise(NoMethodError) { @member.club.private_method }
end
def test_has_one_through_proxy_should_respond_to_private_methods_via_send
@@ -173,4 +173,20 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_not_nil assert_no_queries { @new_detail.member_type }
end
+ def test_save_of_record_with_loaded_has_one_through
+ @club = @member.club
+ assert_not_nil @club.sponsored_member
+
+ assert_nothing_raised do
+ Club.find(@club.id).save!
+ Club.find(@club.id, :include => :sponsored_member).save!
+ end
+
+ @club.sponsor.destroy
+
+ assert_nothing_raised do
+ Club.find(@club.id).save!
+ Club.find(@club.id, :include => :sponsored_member).save!
+ end
+ end
end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 759f9f3872..17ed302465 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -100,7 +100,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
%w(save create_or_update).each do |method|
klass = Class.new ActiveRecord::Base
klass.class_eval "def #{method}() 'defined #{method}' end"
- assert_raises ActiveRecord::DangerousAttributeError do
+ assert_raise ActiveRecord::DangerousAttributeError do
klass.instance_method_already_implemented?(method)
end
end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 381249c0c2..436f50d395 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -1,10 +1,17 @@
-require "cases/helper"
-require "models/pirate"
-require "models/ship"
-require "models/ship_part"
-require "models/bird"
-require "models/parrot"
-require "models/treasure"
+require 'cases/helper'
+require 'models/bird'
+require 'models/company'
+require 'models/customer'
+require 'models/developer'
+require 'models/order'
+require 'models/parrot'
+require 'models/person'
+require 'models/pirate'
+require 'models/post'
+require 'models/reader'
+require 'models/ship'
+require 'models/ship_part'
+require 'models/treasure'
class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
def test_autosave_should_be_a_valid_option_for_has_one
@@ -30,6 +37,383 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
end
end
+class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
+ def test_save_fails_for_invalid_has_one
+ firm = Firm.find(:first)
+ assert firm.valid?
+
+ firm.account = Account.new
+
+ assert !firm.account.valid?
+ assert !firm.valid?
+ assert !firm.save
+ assert_equal "is invalid", firm.errors.on("account")
+ end
+
+ def test_save_succeeds_for_invalid_has_one_with_validate_false
+ firm = Firm.find(:first)
+ assert firm.valid?
+
+ firm.unvalidated_account = Account.new
+
+ assert !firm.unvalidated_account.valid?
+ assert firm.valid?
+ assert firm.save
+ end
+
+ def test_build_before_child_saved
+ firm = Firm.find(1)
+
+ account = firm.account.build("credit_limit" => 1000)
+ assert_equal account, firm.account
+ assert account.new_record?
+ assert firm.save
+ assert_equal account, firm.account
+ assert !account.new_record?
+ end
+
+ def test_build_before_either_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+
+ firm.account = account = Account.new("credit_limit" => 1000)
+ assert_equal account, firm.account
+ assert account.new_record?
+ assert firm.save
+ assert_equal account, firm.account
+ assert !account.new_record?
+ end
+
+ def test_assignment_before_parent_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.account = a = Account.find(1)
+ assert firm.new_record?
+ assert_equal a, firm.account
+ assert firm.save
+ assert_equal a, firm.account
+ assert_equal a, firm.account(true)
+ end
+
+ def test_assignment_before_either_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.account = a = Account.new("credit_limit" => 1000)
+ assert firm.new_record?
+ assert a.new_record?
+ assert_equal a, firm.account
+ assert firm.save
+ assert !firm.new_record?
+ assert !a.new_record?
+ assert_equal a, firm.account
+ assert_equal a, firm.account(true)
+ end
+
+ def test_not_resaved_when_unchanged
+ firm = Firm.find(:first, :include => :account)
+ firm.name += '-changed'
+ assert_queries(1) { firm.save! }
+
+ firm = Firm.find(:first)
+ firm.account = Account.find(:first)
+ assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! }
+
+ firm = Firm.find(:first).clone
+ firm.account = Account.find(:first)
+ assert_queries(2) { firm.save! }
+
+ firm = Firm.find(:first).clone
+ firm.account = Account.find(:first).clone
+ assert_queries(2) { firm.save! }
+ end
+end
+
+class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
+ def test_save_fails_for_invalid_belongs_to
+ assert log = AuditLog.create(:developer_id => 0, :message => "")
+
+ log.developer = Developer.new
+ assert !log.developer.valid?
+ assert !log.valid?
+ assert !log.save
+ assert_equal "is invalid", log.errors.on("developer")
+ end
+
+ def test_save_succeeds_for_invalid_belongs_to_with_validate_false
+ assert log = AuditLog.create(:developer_id => 0, :message=> "")
+
+ log.unvalidated_developer = Developer.new
+ assert !log.unvalidated_developer.valid?
+ assert log.valid?
+ assert log.save
+ end
+
+ def test_assignment_before_parent_saved
+ client = Client.find(:first)
+ apple = Firm.new("name" => "Apple")
+ client.firm = apple
+ assert_equal apple, client.firm
+ assert apple.new_record?
+ assert client.save
+ assert apple.save
+ assert !apple.new_record?
+ assert_equal apple, client.firm
+ assert_equal apple, client.firm(true)
+ end
+
+ def test_assignment_before_either_saved
+ final_cut = Client.new("name" => "Final Cut")
+ apple = Firm.new("name" => "Apple")
+ final_cut.firm = apple
+ assert final_cut.new_record?
+ assert apple.new_record?
+ assert final_cut.save
+ assert !final_cut.new_record?
+ assert !apple.new_record?
+ assert_equal apple, final_cut.firm
+ assert_equal apple, final_cut.firm(true)
+ end
+
+ def test_store_two_association_with_one_save
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.new
+
+ customer1 = order.billing = Customer.new
+ customer2 = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer1, order.billing
+ assert_equal customer2, order.shipping
+
+ order.reload
+
+ assert_equal customer1, order.billing
+ assert_equal customer2, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +2, Customer.count
+ end
+
+ def test_store_association_in_two_relations_with_one_save
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.new
+
+ customer = order.billing = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ order.reload
+
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +1, Customer.count
+ end
+
+ def test_store_association_in_two_relations_with_one_save_in_existing_object
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.create
+
+ customer = order.billing = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ order.reload
+
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +1, Customer.count
+ end
+
+ def test_store_association_in_two_relations_with_one_save_in_existing_object_with_values
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.create
+
+ customer = order.billing = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ order.reload
+
+ customer = order.billing = order.shipping = Customer.new
+
+ assert order.save
+ order.reload
+
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +2, Customer.count
+ end
+end
+
+class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
+ fixtures :companies, :people
+
+ def test_invalid_adding
+ firm = Firm.find(1)
+ assert !(firm.clients_of_firm << c = Client.new)
+ assert c.new_record?
+ assert !firm.valid?
+ assert !firm.save
+ assert c.new_record?
+ end
+
+ def test_invalid_adding_before_save
+ no_of_firms = Firm.count
+ no_of_clients = Client.count
+ new_firm = Firm.new("name" => "A New Firm, Inc")
+ new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
+ assert c.new_record?
+ assert !c.valid?
+ assert !new_firm.valid?
+ assert !new_firm.save
+ assert c.new_record?
+ assert new_firm.new_record?
+ end
+
+ def test_invalid_adding_with_validate_false
+ firm = Firm.find(:first)
+ client = Client.new
+ firm.unvalidated_clients_of_firm << client
+
+ assert firm.valid?
+ assert !client.valid?
+ assert firm.save
+ assert client.new_record?
+ end
+
+ def test_valid_adding_with_validate_false
+ no_of_clients = Client.count
+
+ firm = Firm.find(:first)
+ client = Client.new("name" => "Apple")
+
+ assert firm.valid?
+ assert client.valid?
+ assert client.new_record?
+
+ firm.unvalidated_clients_of_firm << client
+
+ assert firm.save
+ assert !client.new_record?
+ assert_equal no_of_clients+1, Client.count
+ end
+
+ def test_invalid_build
+ new_client = companies(:first_firm).clients_of_firm.build
+ assert new_client.new_record?
+ assert !new_client.valid?
+ assert_equal new_client, companies(:first_firm).clients_of_firm.last
+ assert !companies(:first_firm).save
+ assert new_client.new_record?
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_adding_before_save
+ no_of_firms = Firm.count
+ no_of_clients = Client.count
+
+ new_firm = Firm.new("name" => "A New Firm, Inc")
+ c = Client.new("name" => "Apple")
+
+ new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
+ assert_equal 1, new_firm.clients_of_firm.size
+ new_firm.clients_of_firm << c
+ assert_equal 2, new_firm.clients_of_firm.size
+
+ assert_equal no_of_firms, Firm.count # Firm was not saved to database.
+ assert_equal no_of_clients, Client.count # Clients were not saved to database.
+ assert new_firm.save
+ assert !new_firm.new_record?
+ assert !c.new_record?
+ assert_equal new_firm, c.firm
+ assert_equal no_of_firms+1, Firm.count # Firm was saved to database.
+ assert_equal no_of_clients+2, Client.count # Clients were saved to database.
+
+ assert_equal 2, new_firm.clients_of_firm.size
+ assert_equal 2, new_firm.clients_of_firm(true).size
+ end
+
+ def test_assign_ids
+ firm = Firm.new("name" => "Apple")
+ firm.client_ids = [companies(:first_client).id, companies(:second_client).id]
+ firm.save
+ firm.reload
+ assert_equal 2, firm.clients.length
+ assert firm.clients.include?(companies(:second_client))
+ end
+
+ def test_assign_ids_for_through_a_belongs_to
+ post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!")
+ post.person_ids = [people(:david).id, people(:michael).id]
+ post.save
+ post.reload
+ assert_equal 2, post.people.length
+ assert post.people.include?(people(:david))
+ end
+
+ def test_build_before_save
+ company = companies(:first_firm)
+ new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
+ assert !company.clients_of_firm.loaded?
+
+ company.name += '-changed'
+ assert_queries(2) { assert company.save }
+ assert !new_client.new_record?
+ assert_equal 2, company.clients_of_firm(true).size
+ end
+
+ def test_build_many_before_save
+ company = companies(:first_firm)
+ new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
+
+ company.name += '-changed'
+ assert_queries(3) { assert company.save }
+ assert_equal 3, company.clients_of_firm(true).size
+ end
+
+ def test_build_via_block_before_save
+ company = companies(:first_firm)
+ new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } }
+ assert !company.clients_of_firm.loaded?
+
+ company.name += '-changed'
+ assert_queries(2) { assert company.save }
+ assert !new_client.new_record?
+ assert_equal 2, company.clients_of_firm(true).size
+ end
+
+ def test_build_many_via_block_before_save
+ company = companies(:first_firm)
+ new_clients = assert_no_queries do
+ company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client|
+ client.name = "changed"
+ end
+ end
+
+ company.name += '-changed'
+ assert_queries(3) { assert company.save }
+ assert_equal 3, company.clients_of_firm(true).size
+ end
+
+ def test_replace_on_new_object
+ firm = Firm.new("name" => "New Firm")
+ firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
+ assert firm.save
+ firm.reload
+ assert_equal 2, firm.clients.length
+ assert firm.clients.include?(Client.find_by_name("New Client"))
+ end
+end
+
class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false
@@ -62,6 +446,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
assert_nil Ship.find_by_id(id)
end
+ def test_should_skip_validation_on_a_child_association_if_marked_for_destruction
+ @pirate.ship.name = ''
+ assert !@pirate.valid?
+
+ @pirate.ship.mark_for_destruction
+ assert_difference('Ship.count', -1) { @pirate.save! }
+ end
+
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_child
# Stub the save method of the @pirate.ship instance to destroy and then raise an exception
class << @pirate.ship
@@ -91,6 +483,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
assert_nil Pirate.find_by_id(id)
end
+ def test_should_skip_validation_on_a_parent_association_if_marked_for_destruction
+ @ship.pirate.catchphrase = ''
+ assert !@ship.valid?
+
+ @ship.pirate.mark_for_destruction
+ assert_difference('Pirate.count', -1) { @ship.save! }
+ end
+
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_parent
# Stub the save method of the @ship.pirate instance to destroy and then raise an exception
class << @ship.pirate
@@ -124,6 +524,17 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
ids.each { |id| assert_nil klass.find_by_id(id) }
end
+ define_method("test_should_skip_validation_on_the_#{association_name}_association_if_marked_for_destruction") do
+ 2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") }
+ children = @pirate.send(association_name)
+
+ children.each { |child| child.name = '' }
+ assert !@pirate.valid?
+
+ children.each { |child| child.mark_for_destruction }
+ assert_difference("#{association_name.classify}.count", -2) { @pirate.save! }
+ end
+
define_method("test_should_rollback_destructions_if_an_exception_occurred_while_saving_#{association_name}") do
2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") }
before = @pirate.send(association_name).map { |c| c }
@@ -145,6 +556,41 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
assert_raise(RuntimeError) { assert !@pirate.save }
assert_equal before, @pirate.reload.send(association_name)
end
+
+ # Add and remove callbacks tests for association collections.
+ %w{ method proc }.each do |callback_type|
+ define_method("test_should_run_add_callback_#{callback_type}s_for_#{association_name}") do
+ association_name_with_callbacks = "#{association_name}_with_#{callback_type}_callbacks"
+
+ pirate = Pirate.new(:catchphrase => "Arr")
+ pirate.send(association_name_with_callbacks).build(:name => "Crowe the One-Eyed")
+
+ expected = [
+ "before_adding_#{callback_type}_#{association_name.singularize}_<new>",
+ "after_adding_#{callback_type}_#{association_name.singularize}_<new>"
+ ]
+
+ assert_equal expected, pirate.ship_log
+ end
+
+ define_method("test_should_run_remove_callback_#{callback_type}s_for_#{association_name}") do
+ association_name_with_callbacks = "#{association_name}_with_#{callback_type}_callbacks"
+
+ @pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed")
+ @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction }
+ child_id = @pirate.send(association_name_with_callbacks).first.id
+
+ @pirate.ship_log.clear
+ @pirate.save
+
+ expected = [
+ "before_removing_#{callback_type}_#{association_name.singularize}_#{child_id}",
+ "after_removing_#{callback_type}_#{association_name.singularize}_#{child_id}"
+ ]
+
+ assert_equal expected, @pirate.ship_log
+ end
+ end
end
end
@@ -181,6 +627,14 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
assert !@pirate.errors.on(:ship_name).blank?
end
+ def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
+ @pirate.ship.name = nil
+ @pirate.catchphrase = nil
+ assert !@pirate.valid?
+ assert !@pirate.errors.on(:ship_name).blank?
+ assert !@pirate.errors.on(:catchphrase).blank?
+ end
+
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@pirate.catchphrase = ''
@pirate.ship.name = ''
@@ -263,6 +717,14 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
assert !@ship.errors.on(:pirate_catchphrase).blank?
end
+ def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid
+ @ship.name = nil
+ @ship.pirate.catchphrase = nil
+ assert !@ship.valid?
+ assert !@ship.errors.on(:name).blank?
+ assert !@ship.errors.on(:pirate_catchphrase).blank?
+ end
+
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@ship.pirate.catchphrase = ''
@ship.name = ''
@@ -326,7 +788,24 @@ module AutosaveAssociationOnACollectionAssociationTests
assert @pirate.errors.on(@association_name).blank?
end
- def test_should_still_allow_to_bypass_validations_on_the_associated_models
+ def test_should_not_use_default_invalid_error_on_associated_models
+ @pirate.send(@association_name).build(:name => '')
+
+ assert !@pirate.valid?
+ assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
+ assert @pirate.errors.on(@association_name).blank?
+ end
+
+ def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
+ @pirate.send(@association_name).each { |child| child.name = '' }
+ @pirate.catchphrase = nil
+
+ assert !@pirate.valid?
+ assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
+ assert !@pirate.errors.on(:catchphrase).blank?
+ end
+
+ def test_should_allow_to_bypass_validations_on_the_associated_models_on_update
@pirate.catchphrase = ''
@pirate.send(@association_name).each { |child| child.name = '' }
@@ -338,6 +817,20 @@ module AutosaveAssociationOnACollectionAssociationTests
]
end
+ def test_should_validation_the_associated_models_on_create
+ assert_no_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count") do
+ 2.times { @pirate.send(@association_name).build }
+ @pirate.save(true)
+ end
+ end
+
+ def test_should_allow_to_bypass_validations_on_the_associated_models_on_create
+ assert_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count", +2) do
+ 2.times { @pirate.send(@association_name).build }
+ @pirate.save(false)
+ end
+ end
+
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@pirate.catchphrase, *@pirate.send(@association_name).map(&:name)]
new_names = ['Grace OMalley', 'Privateers Greed']
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 973bb567bd..99d77961fc 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -424,8 +424,8 @@ class BasicsTest < ActiveRecord::TestCase
def test_non_attribute_access_and_assignment
topic = Topic.new
assert !topic.respond_to?("mumbo")
- assert_raises(NoMethodError) { topic.mumbo }
- assert_raises(NoMethodError) { topic.mumbo = 5 }
+ assert_raise(NoMethodError) { topic.mumbo }
+ assert_raise(NoMethodError) { topic.mumbo = 5 }
end
def test_preserving_date_objects
@@ -490,7 +490,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_record_not_found_exception
- assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) }
+ assert_raise(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) }
end
def test_initialize_with_attributes
@@ -848,7 +848,7 @@ class BasicsTest < ActiveRecord::TestCase
client.delete
assert client.frozen?
assert_kind_of Firm, client.firm
- assert_raises(ActiveSupport::FrozenObjectError) { client.name = "something else" }
+ assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" }
end
def test_destroy_new_record
@@ -862,7 +862,7 @@ class BasicsTest < ActiveRecord::TestCase
client.destroy
assert client.frozen?
assert_kind_of Firm, client.firm
- assert_raises(ActiveSupport::FrozenObjectError) { client.name = "something else" }
+ assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" }
end
def test_update_attribute
@@ -910,8 +910,8 @@ class BasicsTest < ActiveRecord::TestCase
def test_mass_assignment_should_raise_exception_if_accessible_and_protected_attribute_writers_are_both_used
topic = TopicWithProtectedContentAndAccessibleAuthorName.new
- assert_raises(RuntimeError) { topic.attributes = { "author_name" => "me" } }
- assert_raises(RuntimeError) { topic.attributes = { "content" => "stuff" } }
+ assert_raise(RuntimeError) { topic.attributes = { "author_name" => "me" } }
+ assert_raise(RuntimeError) { topic.attributes = { "content" => "stuff" } }
end
def test_mass_assignment_protection
@@ -949,7 +949,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_mass_assigning_invalid_attribute
firm = Firm.new
- assert_raises(ActiveRecord::UnknownAttributeError) do
+ assert_raise(ActiveRecord::UnknownAttributeError) do
firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 }
end
end
@@ -1402,7 +1402,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_sql_injection_via_find
- assert_raises(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do
Topic.find("123456 OR id > 0")
end
end
@@ -1755,6 +1755,13 @@ class BasicsTest < ActiveRecord::TestCase
end
end
+ def test_scoped_find_with_group_and_having
+ developers = Developer.with_scope(:find => { :group => 'salary', :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" }) do
+ Developer.find(:all)
+ end
+ assert_equal 3, developers.size
+ end
+
def test_find_last
last = Developer.find :last
assert_equal last, Developer.find(:first, :order => 'id desc')
@@ -1783,6 +1790,11 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal last, Developer.find(:all, :order => 'developers.name, developers.salary DESC').last
end
+ def test_find_symbol_ordered_last
+ last = Developer.find :last, :order => :salary
+ assert_equal last, Developer.find(:all, :order => :salary).last
+ end
+
def test_find_scoped_ordered_last
last_developer = Developer.with_scope(:find => { :order => 'developers.salary ASC' }) do
Developer.find(:last)
@@ -2092,18 +2104,4 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal custom_datetime, parrot[attribute]
end
end
-
- private
- def with_kcode(kcode)
- if RUBY_VERSION < '1.9'
- orig_kcode, $KCODE = $KCODE, kcode
- begin
- yield
- ensure
- $KCODE = orig_kcode
- end
- else
- yield
- end
- end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index 108d679108..5009a90846 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -11,7 +11,7 @@ class EachTest < ActiveRecord::TestCase
def test_each_should_excecute_one_query_per_batch
assert_queries(Post.count + 1) do
- Post.each(:batch_size => 1) do |post|
+ Post.find_each(:batch_size => 1) do |post|
assert_kind_of Post, post
end
end
@@ -19,13 +19,13 @@ class EachTest < ActiveRecord::TestCase
def test_each_should_raise_if_the_order_is_set
assert_raise(RuntimeError) do
- Post.each(:order => "title") { |post| post }
+ Post.find_each(:order => "title") { |post| post }
end
end
def test_each_should_raise_if_the_limit_is_set
assert_raise(RuntimeError) do
- Post.each(:limit => 1) { |post| post }
+ Post.find_each(:limit => 1) { |post| post }
end
end
@@ -46,4 +46,16 @@ class EachTest < ActiveRecord::TestCase
end
end
end
+
+ def test_find_in_batches_shouldnt_excute_query_unless_needed
+ post_count = Post.count
+
+ assert_queries(2) do
+ Post.find_in_batches(:batch_size => post_count) {|batch| assert_kind_of Array, batch }
+ end
+
+ assert_queries(1) do
+ Post.find_in_batches(:batch_size => post_count + 1) {|batch| assert_kind_of Array, batch }
+ end
+ end
end \ No newline at end of file
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 5a4ed429b6..56dcdea110 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -92,6 +92,14 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 60, c[2]
end
+ def test_should_group_by_summed_field_having_sanitized_condition
+ c = Account.sum(:credit_limit, :group => :firm_id,
+ :having => ['sum(credit_limit) > ?', 50])
+ assert_nil c[1]
+ assert_equal 105, c[6]
+ assert_equal 60, c[2]
+ end
+
def test_should_group_by_summed_association
c = Account.sum(:credit_limit, :group => :firm)
assert_equal 50, c[companies(:first_firm)]
@@ -247,8 +255,8 @@ class CalculationsTest < ActiveRecord::TestCase
Company.send(:validate_calculation_options, :count, :include => true)
end
- assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
- assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :count, :foo => :bar) }
+ assert_raise(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
+ assert_raise(ArgumentError) { Company.send(:validate_calculation_options, :count, :foo => :bar) }
end
def test_should_count_selected_field_with_include
@@ -256,6 +264,19 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 4, Account.count(:distinct => true, :include => :firm, :select => :credit_limit)
end
+ def test_should_count_scoped_select
+ Account.update_all("credit_limit = NULL")
+ assert_equal 0, Account.scoped(:select => "credit_limit").count
+ end
+
+ def test_should_count_scoped_select_with_options
+ Account.update_all("credit_limit = NULL")
+ Account.last.update_attribute('credit_limit', 49)
+ Account.first.update_attribute('credit_limit', 51)
+
+ assert_equal 1, Account.scoped(:select => "credit_limit").count(:conditions => ['credit_limit >= 50'])
+ end
+
def test_should_count_manual_select_with_include
assert_equal 6, Account.count(:select => "DISTINCT accounts.id", :include => :firm)
end
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 33b1ea034d..95fddaeef6 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -352,13 +352,13 @@ class CallbacksTest < ActiveRecord::TestCase
david = ImmutableDeveloper.find(1)
assert david.valid?
assert !david.save
- assert_raises(ActiveRecord::RecordNotSaved) { david.save! }
+ assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
david = ImmutableDeveloper.find(1)
david.salary = 10_000_000
assert !david.valid?
assert !david.save
- assert_raises(ActiveRecord::RecordInvalid) { david.save! }
+ assert_raise(ActiveRecord::RecordInvalid) { david.save! }
someone = CallbackCancellationDeveloper.find(1)
someone.cancel_before_save = true
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
new file mode 100644
index 0000000000..cc9b2a45f4
--- /dev/null
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -0,0 +1,25 @@
+require "cases/helper"
+
+class ConnectionManagementTest < ActiveRecord::TestCase
+ def setup
+ @env = {}
+ @app = stub('App')
+ @management = ActiveRecord::ConnectionAdapters::ConnectionManagement.new(@app)
+
+ @connections_cleared = false
+ ActiveRecord::Base.stubs(:clear_active_connections!).with { @connections_cleared = true }
+ end
+
+ test "clears active connections after each call" do
+ @app.expects(:call).with(@env)
+ @management.call(@env)
+ assert @connections_cleared
+ end
+
+ test "doesn't clear active connections when running in a test case" do
+ @env['rack.test'] = true
+ @app.expects(:call).with(@env)
+ @management.call(@env)
+ assert !@connections_cleared
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 5f5707b388..ac95bac4ad 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -228,7 +228,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.new
pirate.parrot_id = 1
- assert_raises(ActiveRecord::RecordInvalid) { pirate.save! }
+ assert_raise(ActiveRecord::RecordInvalid) { pirate.save! }
check_pirate_after_save_failure(pirate)
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index ee8f4901f9..d8778957c0 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -146,7 +146,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_ids_missing_one
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) }
end
def test_find_all_with_limit
@@ -191,6 +191,13 @@ class FinderTest < ActiveRecord::TestCase
assert developers.all? { |developer| developer.salary > 10000 }
end
+ def test_find_with_group_and_sanitized_having
+ developers = Developer.find(:all, :group => "salary", :having => ["sum(salary) > ?", 10000], :select => "salary")
+ assert_equal 3, developers.size
+ assert_equal 3, developers.map(&:salary).uniq.size
+ assert developers.all? { |developer| developer.salary > 10000 }
+ end
+
def test_find_with_entire_select_statement
topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'"
@@ -229,7 +236,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_unexisting_record_exception_handling
- assert_raises(ActiveRecord::RecordNotFound) {
+ assert_raise(ActiveRecord::RecordNotFound) {
Topic.find(1).parent
}
@@ -238,7 +245,7 @@ class FinderTest < ActiveRecord::TestCase
def test_find_only_some_columns
topic = Topic.find(1, :select => "author_name")
- assert_raises(ActiveRecord::MissingAttributeError) {topic.title}
+ assert_raise(ActiveRecord::MissingAttributeError) {topic.title}
assert_equal "David", topic.author_name
assert !topic.attribute_present?("title")
#assert !topic.respond_to?("title")
@@ -260,22 +267,22 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_array_conditions
assert Topic.find(1, :conditions => ["approved = ?", false])
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) }
end
def test_find_on_hash_conditions
assert Topic.find(1, :conditions => { :approved => false })
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) }
end
def test_find_on_hash_conditions_with_explicit_table_name
assert Topic.find(1, :conditions => { 'topics.approved' => false })
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) }
end
def test_find_on_hash_conditions_with_hashed_table_name
assert Topic.find(1, :conditions => {:topics => { :approved => false }})
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) }
end
def test_find_with_hash_conditions_on_joined_table
@@ -293,7 +300,7 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate
david = customers(:david)
assert Customer.find(david.id, :conditions => { 'customers.name' => david.name, :address => david.address })
- assert_raises(ActiveRecord::RecordNotFound) {
+ assert_raise(ActiveRecord::RecordNotFound) {
Customer.find(david.id, :conditions => { 'customers.name' => david.name + "1", :address => david.address })
}
end
@@ -304,13 +311,13 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_hash_conditions_with_range
assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1..2 }).map(&:id).sort
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :id => 2..3 }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :id => 2..3 }) }
end
def test_find_on_hash_conditions_with_end_exclusive_range
assert_equal [1,2,3], Topic.find(:all, :conditions => { :id => 1..3 }).map(&:id).sort
assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1...3 }).map(&:id).sort
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(3, :conditions => { :id => 2...3 }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(3, :conditions => { :id => 2...3 }) }
end
def test_find_on_hash_conditions_with_multiple_ranges
@@ -320,9 +327,9 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_multiple_hash_conditions
assert Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false })
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) }
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
end
def test_condition_interpolation
@@ -346,7 +353,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_hash_condition_find_malformed
- assert_raises(ActiveRecord::StatementInvalid) {
+ assert_raise(ActiveRecord::StatementInvalid) {
Company.find(:first, :conditions => { :id => 2, :dhh => true })
}
end
@@ -415,10 +422,10 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"])
assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!' OR 1=1"])
assert_kind_of Time, Topic.find(:first, :conditions => ["id = ?", 1]).written_on
- assert_raises(ActiveRecord::PreparedStatementInvalid) {
+ assert_raise(ActiveRecord::PreparedStatementInvalid) {
Company.find(:first, :conditions => ["id=? AND name = ?", 2])
}
- assert_raises(ActiveRecord::PreparedStatementInvalid) {
+ assert_raise(ActiveRecord::PreparedStatementInvalid) {
Company.find(:first, :conditions => ["id=?", 2, 3, 4])
}
end
@@ -435,11 +442,11 @@ class FinderTest < ActiveRecord::TestCase
def test_bind_arity
assert_nothing_raised { bind '' }
- assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
- assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?' }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' }
assert_nothing_raised { bind '?', 1 }
- assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
end
def test_named_bind_variables
@@ -544,7 +551,7 @@ class FinderTest < ActiveRecord::TestCase
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!") }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
end
def test_find_by_one_attribute_caches_dynamic_finder
@@ -625,14 +632,14 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_one_missing_attribute
- assert_raises(NoMethodError) { Topic.find_by_undertitle("The First Topic!") }
+ assert_raise(NoMethodError) { Topic.find_by_undertitle("The First Topic!") }
end
def test_find_by_invalid_method_syntax
- assert_raises(NoMethodError) { Topic.fail_to_find_by_title("The First Topic") }
- assert_raises(NoMethodError) { Topic.find_by_title?("The First Topic") }
- assert_raises(NoMethodError) { Topic.fail_to_find_or_create_by_title("Nonexistent Title") }
- assert_raises(NoMethodError) { Topic.find_or_create_by_title?("Nonexistent Title") }
+ assert_raise(NoMethodError) { Topic.fail_to_find_by_title("The First Topic") }
+ assert_raise(NoMethodError) { Topic.find_by_title?("The First Topic") }
+ assert_raise(NoMethodError) { Topic.fail_to_find_or_create_by_title("Nonexistent Title") }
+ assert_raise(NoMethodError) { Topic.find_or_create_by_title?("Nonexistent Title") }
end
def test_find_by_two_attributes
@@ -654,8 +661,8 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_last_by_invalid_method_syntax
- assert_raises(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") }
- assert_raises(NoMethodError) { Topic.find_last_by_title?("The First Topic") }
+ assert_raise(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") }
+ assert_raise(NoMethodError) { Topic.find_last_by_title?("The First Topic") }
end
def test_find_last_by_one_attribute_with_several_options
@@ -663,7 +670,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_last_by_one_missing_attribute
- assert_raises(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") }
+ assert_raise(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") }
end
def test_find_last_by_two_attributes
@@ -916,16 +923,16 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_bad_sql
- assert_raises(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" }
+ assert_raise(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" }
end
def test_find_with_invalid_params
- assert_raises(ArgumentError) { Topic.find :first, :join => "It should be `joins'" }
- assert_raises(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" }
+ assert_raise(ArgumentError) { Topic.find :first, :join => "It should be `joins'" }
+ assert_raise(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" }
end
def test_dynamic_finder_with_invalid_params
- assert_raises(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" }
+ assert_raise(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" }
end
def test_find_all_with_limit
@@ -1057,6 +1064,14 @@ class FinderTest < ActiveRecord::TestCase
assert_equal [0, 1, 1], posts.map(&:author_id).sort
end
+ def test_finder_with_scoped_from
+ all_topics = Topic.all
+
+ Topic.with_scope(:find => { :from => 'fake_topics' }) do
+ assert_equal all_topics, Topic.all(:from => 'topics')
+ end
+ end
+
protected
def bind(statement, *vars)
if vars.first.is_a?(Hash)
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index eb63fd5f34..252bf4ff61 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -151,7 +151,7 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_dirty_dirty_yaml_file
- assert_raises(Fixture::FormatError) do
+ assert_raise(Fixture::FormatError) do
Fixtures.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses")
end
end
@@ -420,7 +420,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def test_raises_error
- assert_raises FixtureClassNotFound do
+ assert_raise FixtureClassNotFound do
funny_jokes(:a_joke)
end
end
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 3f59eb9706..eae5a60829 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -68,7 +68,7 @@ class InheritanceTest < ActiveRecord::TestCase
if current_adapter?(:SybaseAdapter)
Company.connection.execute "SET IDENTITY_INSERT companies OFF"
end
- assert_raises(ActiveRecord::SubclassNotFound) { Company.find(100) }
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.find(100) }
end
def test_inheritance_find
@@ -124,7 +124,7 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_finding_incorrect_type_data
- assert_raises(ActiveRecord::RecordNotFound) { Firm.find(2) }
+ assert_raise(ActiveRecord::RecordNotFound) { Firm.find(2) }
assert_nothing_raised { Firm.find(1) }
end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 077cac7747..e177235591 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -35,7 +35,25 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
- assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
+ end
+
+ def test_lock_destroy
+ p1 = Person.find(1)
+ p2 = Person.find(1)
+ assert_equal 0, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ p1.first_name = 'stu'
+ p1.save!
+ assert_equal 1, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
+
+ assert p1.destroy
+ assert_equal true, p1.frozen?
+ assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
end
def test_lock_repeating
@@ -50,9 +68,9 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
- assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
p2.first_name = 'sue2'
- assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
def test_lock_new
@@ -71,7 +89,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
- assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
def test_lock_new_with_nil
@@ -95,7 +113,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, t2.version
t2.tps_report_number = 800
- assert_raises(ActiveRecord::StaleObjectError) { t2.save! }
+ assert_raise(ActiveRecord::StaleObjectError) { t2.save! }
end
def test_lock_column_is_mass_assignable
diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb
index 71e2ce8790..3c34cdeade 100644
--- a/activerecord/test/cases/method_scoping_test.rb
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -262,6 +262,15 @@ class NestedScopingTest < ActiveRecord::TestCase
end
end
+ def test_merge_inner_scope_has_priority
+ Developer.with_scope(:find => { :limit => 5 }) do
+ Developer.with_scope(:find => { :limit => 10 }) do
+ merged_option = Developer.instance_eval('current_scoped_methods')[:find]
+ assert_equal({ :limit => 10 }, merged_option)
+ end
+ end
+ end
+
def test_replace_options
Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do
@@ -369,8 +378,10 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_merged_scoped_find
poor_jamis = developers(:poor_jamis)
Developer.with_scope(:find => { :conditions => "salary < 100000" }) do
- Developer.with_scope(:find => { :offset => 1 }) do
- assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc'))
+ Developer.with_scope(:find => { :offset => 1, :order => 'id asc' }) do
+ assert_sql /ORDER BY id asc / do
+ assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc'))
+ end
end
end
end
@@ -400,6 +411,29 @@ class NestedScopingTest < ActiveRecord::TestCase
end
end
+ def test_nested_scoped_create
+ comment = nil
+ Comment.with_scope(:create => { :post_id => 1}) do
+ Comment.with_scope(:create => { :post_id => 2}) do
+ assert_equal({ :post_id => 2 }, Comment.send(:current_scoped_methods)[:create])
+ comment = Comment.create :body => "Hey guys, nested scopes are broken. Please fix!"
+ end
+ end
+ assert_equal 2, comment.post_id
+ end
+
+ def test_nested_exclusive_scope_for_create
+ comment = nil
+ Comment.with_scope(:create => { :body => "Hey guys, nested scopes are broken. Please fix!" }) do
+ Comment.with_exclusive_scope(:create => { :post_id => 1 }) do
+ assert_equal({ :post_id => 1 }, Comment.send(:current_scoped_methods)[:create])
+ comment = Comment.create :body => "Hey guys"
+ end
+ end
+ assert_equal 1, comment.post_id
+ assert_equal 'Hey guys', comment.body
+ end
+
def test_merged_scoped_find_on_blank_conditions
[nil, " ", [], {}].each do |blank|
Developer.with_scope(:find => {:conditions => blank}) do
@@ -523,7 +557,6 @@ class HasManyScopingTest< ActiveRecord::TestCase
end
end
-
class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
fixtures :posts, :categories, :categories_posts
@@ -549,7 +582,6 @@ class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
end
end
-
class DefaultScopingTest < ActiveRecord::TestCase
fixtures :developers
@@ -577,7 +609,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
# Scopes added on children should append to parent scope
expected_klass_scope = [{ :create => {}, :find => { :order => 'salary DESC' }}, { :create => {}, :find => {} }]
assert_equal expected_klass_scope, klass.send(:scoped_methods)
-
+
# Parent should still have the original scope
assert_equal scope, DeveloperOrderedBySalary.send(:scoped_methods)
end
@@ -597,7 +629,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_named_scope
- expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary }
+ expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.salary }
assert_equal expected, received
end
@@ -620,7 +652,6 @@ end
=begin
# We disabled the scoping for has_one and belongs_to as we can't think of a proper use case
-
class BelongsToScopingTest< ActiveRecord::TestCase
fixtures :comments, :posts
@@ -640,7 +671,6 @@ class BelongsToScopingTest< ActiveRecord::TestCase
end
-
class HasOneScopingTest< ActiveRecord::TestCase
fixtures :comments, :posts
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 1c974e4825..16861f21b1 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -93,6 +93,30 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
+ def testing_table_with_only_foo_attribute
+ Person.connection.create_table :testings, :id => false do |t|
+ t.column :foo, :string
+ end
+
+ yield Person.connection
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+ protected :testing_table_with_only_foo_attribute
+
+ def test_create_table_without_id
+ testing_table_with_only_foo_attribute do |connection|
+ assert_equal connection.columns(:testings).size, 1
+ end
+ end
+
+ def test_add_column_with_primary_key_attribute
+ testing_table_with_only_foo_attribute do |connection|
+ assert_nothing_raised { connection.add_column :testings, :id, :primary_key }
+ assert_equal connection.columns(:testings).size, 2
+ end
+ end
+
def test_create_table_adds_id
Person.connection.create_table :testings do |t|
t.column :foo, :string
@@ -111,7 +135,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
- assert_raises(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::StatementInvalid) do
Person.connection.execute "insert into testings (foo) values (NULL)"
end
ensure
@@ -278,7 +302,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
Person.connection.add_column :testings, :bar, :string, :null => false
- assert_raises(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::StatementInvalid) do
Person.connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
end
ensure
@@ -297,7 +321,7 @@ if ActiveRecord::Base.connection.supports_migrations?
Person.connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter)
assert_nothing_raised {Person.connection.add_column :testings, :bar, :string, :null => false, :default => "default" }
- assert_raises(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::StatementInvalid) do
unless current_adapter?(:OpenBaseAdapter)
Person.connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
else
@@ -545,7 +569,7 @@ if ActiveRecord::Base.connection.supports_migrations?
else
ActiveRecord::ActiveRecordError
end
- assert_raises(exception) do
+ assert_raise(exception) do
Person.connection.rename_column "hats", "nonexistent", "should_fail"
end
ensure
@@ -795,7 +819,7 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal "hello world", Reminder.find(:first).content
WeNeedReminders.down
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
end
def test_add_table_with_decimals
@@ -856,7 +880,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
GiveMeBigNumbers.down
- assert_raises(ActiveRecord::StatementInvalid) { BigNumber.find(:first) }
+ assert_raise(ActiveRecord::StatementInvalid) { BigNumber.find(:first) }
end
def test_migrator
@@ -876,7 +900,7 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal 0, ActiveRecord::Migrator.current_version
Person.reset_column_information
assert !Person.column_methods_hash.include?(:last_name)
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
end
def test_migrator_one_up
@@ -928,11 +952,11 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal(0, ActiveRecord::Migrator.current_version)
end
- if current_adapter?(:PostgreSQLAdapter)
+ if ActiveRecord::Base.connection.supports_ddl_transactions?
def test_migrator_one_up_with_exception_and_rollback
assert !Person.column_methods_hash.include?(:last_name)
- e = assert_raises(StandardError) do
+ e = assert_raise(StandardError) do
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/broken", 100)
end
@@ -945,20 +969,20 @@ if ActiveRecord::Base.connection.supports_migrations?
def test_finds_migrations
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations
- [['1', 'people_have_last_names'],
- ['2', 'we_need_reminders'],
- ['3', 'innocent_jointable']].each_with_index do |pair, i|
- migrations[i].version == pair.first
- migrations[1].name == pair.last
+
+ [[1, 'PeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
+ assert_equal migrations[i].version, pair.first
+ assert_equal migrations[i].name, pair.last
end
end
def test_finds_pending_migrations
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2", 1)
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/interleaved/pass_2").pending_migrations
+
assert_equal 1, migrations.size
- migrations[0].version == '3'
- migrations[0].name == 'innocent_jointable'
+ assert_equal migrations[0].version, 3
+ assert_equal migrations[0].name, 'InnocentJointable'
end
def test_only_loads_pending_migrations
@@ -1107,7 +1131,7 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal "hello world", Reminder.find(:first).content
WeNeedReminders.down
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
ensure
ActiveRecord::Base.table_name_prefix = ''
ActiveRecord::Base.table_name_suffix = ''
@@ -1137,13 +1161,13 @@ if ActiveRecord::Base.connection.supports_migrations?
end
def test_migrator_with_duplicates
- assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
+ assert_raise(ActiveRecord::DuplicateMigrationVersionError) do
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/duplicate", nil)
end
end
def test_migrator_with_duplicate_names
- assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
+ assert_raise(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/duplicate_names", nil)
end
end
@@ -1159,7 +1183,7 @@ if ActiveRecord::Base.connection.supports_migrations?
# table name is 29 chars, the standard sequence name will
# be 33 chars and fail
- assert_raises(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::StatementInvalid) do
begin
Person.connection.create_table :table_with_name_thats_just_ok do |t|
t.column :foo, :string, :null => false
@@ -1186,7 +1210,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
# confirm the custom sequence got dropped
- assert_raises(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::StatementInvalid) do
Person.connection.execute("select suitably_short_seq.nextval from dual")
end
end
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
index e1e27fa130..ae6a54a5bd 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/named_scope_test.rb
@@ -15,7 +15,7 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal Topic.find(:all), Topic.base
assert_equal Topic.find(:all), Topic.base.to_a
assert_equal Topic.find(:first), Topic.base.first
- assert_equal Topic.find(:all), Topic.base.each { |i| i }
+ assert_equal Topic.find(:all), Topic.base.map { |i| i }
end
def test_found_items_are_cached
@@ -99,6 +99,12 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal topics_written_before_the_second, Topic.written_before(topics(:second).written_on)
end
+ def test_procedural_scopes_returning_nil
+ all_topics = Topic.find(:all)
+
+ assert_equal all_topics, Topic.written_before(nil)
+ end
+
def test_scopes_with_joins
address = author_addresses(:david_address)
posts_with_authors_at_address = Post.find(
@@ -142,6 +148,15 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal authors(:david).comments & Comment.containing_the_letter_e, authors(:david).comments.containing_the_letter_e
end
+ def test_named_scopes_honor_current_scopes_from_when_defined
+ assert !Post.ranked_by_comments.limit(5).empty?
+ assert !authors(:david).posts.ranked_by_comments.limit(5).empty?
+ assert_not_equal Post.ranked_by_comments.limit(5), authors(:david).posts.ranked_by_comments.limit(5)
+ assert_not_equal Post.top(5), authors(:david).posts.top(5)
+ assert_equal authors(:david).posts.ranked_by_comments.limit(5), authors(:david).posts.top(5)
+ assert_equal Post.ranked_by_comments.limit(5), Post.top(5)
+ end
+
def test_active_records_have_scope_named__all__
assert !Topic.find(:all).empty?
@@ -238,7 +253,7 @@ class NamedScopeTest < ActiveRecord::TestCase
topic = Topic.approved.create!({})
assert topic.approved
end
-
+
def test_should_build_with_proxy_options_chained
topic = Topic.approved.by_lifo.build({})
assert topic.approved
@@ -278,15 +293,21 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size
end
- def test_chanining_should_use_latest_conditions_when_creating
- post1 = Topic.rejected.approved.new
- assert post1.approved?
+ def test_chaining_should_use_latest_conditions_when_creating
+ post = Topic.rejected.new
+ assert !post.approved?
+
+ post = Topic.rejected.approved.new
+ assert post.approved?
- post2 = Topic.approved.rejected.new
- assert ! post2.approved?
+ post = Topic.approved.rejected.new
+ assert !post.approved?
+
+ post = Topic.approved.rejected.approved.new
+ assert post.approved?
end
- def test_chanining_should_use_latest_conditions_when_searching
+ def test_chaining_should_use_latest_conditions_when_searching
# Normal hash conditions
assert_equal Topic.all(:conditions => {:approved => true}), Topic.rejected.approved.all
assert_equal Topic.all(:conditions => {:approved => false}), Topic.approved.rejected.all
@@ -297,6 +318,24 @@ class NamedScopeTest < ActiveRecord::TestCase
# Nested hash conditions with different keys
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).all.uniq
end
+
+ def test_methods_invoked_within_scopes_should_respect_scope
+ assert_equal [], Topic.approved.by_rejected_ids.proxy_options[:conditions][:id]
+ end
+
+ def test_named_scopes_batch_finders
+ assert_equal 3, Topic.approved.count
+
+ assert_queries(4) do
+ Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? }
+ end
+
+ assert_queries(2) do
+ Topic.approved.find_in_batches(:batch_size => 2) do |group|
+ group.each {|t| assert t.approved? }
+ end
+ end
+ end
end
class DynamicScopeMatchTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 8b1c714ead..db64bbb806 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -4,6 +4,7 @@ require 'models/customer'
require 'models/company'
require 'models/company_in_module'
require 'models/subscriber'
+require 'models/pirate'
class ReflectionTest < ActiveRecord::TestCase
fixtures :topics, :customers, :companies, :subscribers
@@ -169,9 +170,9 @@ class ReflectionTest < ActiveRecord::TestCase
def test_reflection_of_all_associations
# FIXME these assertions bust a lot
- assert_equal 26, Firm.reflect_on_all_associations.size
- assert_equal 20, Firm.reflect_on_all_associations(:has_many).size
- assert_equal 6, Firm.reflect_on_all_associations(:has_one).size
+ assert_equal 28, Firm.reflect_on_all_associations.size
+ assert_equal 21, Firm.reflect_on_all_associations(:has_many).size
+ assert_equal 7, Firm.reflect_on_all_associations(:has_one).size
assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 40abd935df..f6533b5396 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -214,7 +214,7 @@ class TransactionTest < ActiveRecord::TestCase
end
def test_invalid_keys_for_transaction
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
Topic.transaction :nested => true do
end
end
@@ -349,7 +349,7 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- def test_sqlite_add_column_in_transaction_raises_statement_invalid
+ def test_sqlite_add_column_in_transaction
return true unless current_adapter?(:SQLite3Adapter, :SQLiteAdapter)
# Test first if column creation/deletion works correctly when no
@@ -368,10 +368,15 @@ class TransactionTest < ActiveRecord::TestCase
assert !Topic.column_names.include?('stuff')
end
- # Test now inside a transaction: add_column should raise a StatementInvalid
- Topic.transaction do
- assert_raises(ActiveRecord::StatementInvalid) { Topic.connection.add_column('topics', 'stuff', :string) }
- raise ActiveRecord::Rollback
+ if Topic.connection.supports_ddl_transactions?
+ assert_nothing_raised do
+ Topic.transaction { Topic.connection.add_column('topics', 'stuff', :string) }
+ end
+ else
+ Topic.transaction do
+ assert_raise(ActiveRecord::StatementInvalid) { Topic.connection.add_column('topics', 'stuff', :string) }
+ raise ActiveRecord::Rollback
+ end
end
end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index cbb184131f..c20f5ae63e 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -8,6 +8,7 @@ require 'models/warehouse_thing'
require 'models/guid'
require 'models/owner'
require 'models/pet'
+require 'models/event'
# The following methods in Topic are used in test_conditional_validation_*
class Topic
@@ -113,8 +114,8 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_invalid_record_exception
- assert_raises(ActiveRecord::RecordInvalid) { Reply.create! }
- assert_raises(ActiveRecord::RecordInvalid) { Reply.new.save! }
+ assert_raise(ActiveRecord::RecordInvalid) { Reply.create! }
+ assert_raise(ActiveRecord::RecordInvalid) { Reply.new.save! }
begin
r = Reply.new
@@ -126,13 +127,13 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_exception_on_create_bang_many
- assert_raises(ActiveRecord::RecordInvalid) do
+ assert_raise(ActiveRecord::RecordInvalid) do
Reply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
end
end
def test_exception_on_create_bang_with_block
- assert_raises(ActiveRecord::RecordInvalid) do
+ assert_raise(ActiveRecord::RecordInvalid) do
Reply.create!({ "title" => "OK" }) do |r|
r.content = nil
end
@@ -140,7 +141,7 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_exception_on_create_bang_many_with_block
- assert_raises(ActiveRecord::RecordInvalid) do
+ assert_raise(ActiveRecord::RecordInvalid) do
Reply.create!([{ "title" => "OK" }, { "title" => "Wrong Create" }]) do |r|
r.content = nil
end
@@ -149,7 +150,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_scoped_create_without_attributes
Reply.with_scope(:create => {}) do
- assert_raises(ActiveRecord::RecordInvalid) { Reply.create! }
+ assert_raise(ActiveRecord::RecordInvalid) { Reply.create! }
end
end
@@ -169,7 +170,7 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal person.first_name, "Mary", "should be ok when no attributes are passed to create!"
end
end
- end
+ end
def test_single_error_per_attr_iteration
r = Reply.new
@@ -530,6 +531,14 @@ class ValidationsTest < ActiveRecord::TestCase
end
end
+ def test_validate_uniqueness_with_limit
+ # Event.title is limited to 5 characters
+ e1 = Event.create(:title => "abcde")
+ assert e1.valid?, "Could not create an event with a unique, 5 character title"
+ e2 = Event.create(:title => "abcdefgh")
+ assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique"
+ end
+
def test_validate_straight_inheritance_uniqueness
w1 = IneptWizard.create(:name => "Rincewind", :city => "Ankh-Morpork")
assert w1.valid?, "Saving w1"
@@ -1421,6 +1430,17 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal "can't be blank", t.errors.on("title").first
end
+ def test_invalid_should_be_the_opposite_of_valid
+ Topic.validates_presence_of :title
+
+ t = Topic.new
+ assert t.invalid?
+ assert t.errors.invalid?(:title)
+
+ t.title = 'Things are going to change'
+ assert !t.invalid?
+ end
+
# previous implementation of validates_presence_of eval'd the
# string with the wrong binding, this regression test is to
# ensure that it works correctly
@@ -1442,20 +1462,6 @@ class ValidationsTest < ActiveRecord::TestCase
t.author_name = "Hubert J. Farnsworth"
assert t.valid?, "A topic with an important title and author should be valid"
end
-
- private
- def with_kcode(kcode)
- if RUBY_VERSION < '1.9'
- orig_kcode, $KCODE = $KCODE, kcode
- begin
- yield
- ensure
- $KCODE = orig_kcode
- end
- else
- yield
- end
- end
end
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index 39c6ea820d..b49997669e 100644
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -38,11 +38,15 @@ class XmlSerializationTest < ActiveRecord::TestCase
assert_match %r{<CreatedAt}, @xml
end
+ def test_should_allow_skipped_types
+ @xml = Contact.new(:age => 25).to_xml :skip_types => true
+ assert %r{<age>25</age>}.match(@xml)
+ end
+
def test_should_include_yielded_additions
@xml = Contact.new.to_xml do |xml|
xml.creator "David"
end
-
assert_match %r{<creator>David</creator>}, @xml
end
end
@@ -145,6 +149,13 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
assert_match %r{<hello-post type="StiPost">}, xml
end
+ def test_included_associations_should_skip_types
+ xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true
+ assert_match %r{<hello-posts>}, xml
+ assert_match %r{<hello-post>}, xml
+ assert_match %r{<hello-post>}, xml
+ end
+
def test_methods_are_called_on_object
xml = authors(:david).to_xml :methods => :label, :indent => 0
assert_match %r{<label>.*</label>}, xml
diff --git a/activerecord/test/fixtures/toys.yml b/activerecord/test/fixtures/toys.yml
new file mode 100644
index 0000000000..037e335e0a
--- /dev/null
+++ b/activerecord/test/fixtures/toys.yml
@@ -0,0 +1,4 @@
+bone:
+ toy_id: 1
+ name: Bone
+ pet_id: 1
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 3b27a9e272..02a775f9ef 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -37,6 +37,7 @@ class Firm < Company
has_many :clients, :order => "id", :dependent => :destroy, :counter_sql =>
"SELECT COUNT(*) FROM companies WHERE firm_id = 1 " +
"AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )"
+ has_many :unsorted_clients, :class_name => "Client"
has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC"
has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id"
has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false
@@ -69,6 +70,7 @@ class Firm < Company
has_one :account_with_select, :foreign_key => "firm_id", :select => "id, firm_id", :class_name=>'Account'
has_one :readonly_account, :foreign_key => "firm_id", :class_name => "Account", :readonly => true
has_one :account_using_primary_key, :primary_key => "firm_id", :class_name => "Account"
+ has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete
end
class DependentFirm < Company
diff --git a/activerecord/test/models/event.rb b/activerecord/test/models/event.rb
new file mode 100644
index 0000000000..99fa0feeb7
--- /dev/null
+++ b/activerecord/test/models/event.rb
@@ -0,0 +1,3 @@
+class Event < ActiveRecord::Base
+ validates_uniqueness_of :title
+end \ No newline at end of file
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index dbaf2ce688..5760b991ec 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -1,4 +1,5 @@
class Owner < ActiveRecord::Base
set_primary_key :owner_id
has_many :pets
-end \ No newline at end of file
+ has_many :toys, :through => :pets
+end
diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb
index 889ce46f33..dc1a3c5e94 100644
--- a/activerecord/test/models/pet.rb
+++ b/activerecord/test/models/pet.rb
@@ -1,4 +1,5 @@
class Pet < ActiveRecord::Base
set_primary_key :pet_id
belongs_to :owner
-end \ No newline at end of file
+ has_many :toys
+end
diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb
index 7bc50e0e7b..238917bf30 100644
--- a/activerecord/test/models/pirate.rb
+++ b/activerecord/test/models/pirate.rb
@@ -1,16 +1,63 @@
class Pirate < ActiveRecord::Base
belongs_to :parrot
has_and_belongs_to_many :parrots
- has_many :treasures, :as => :looter
+ has_and_belongs_to_many :parrots_with_method_callbacks, :class_name => "Parrot",
+ :before_add => :log_before_add,
+ :after_add => :log_after_add,
+ :before_remove => :log_before_remove,
+ :after_remove => :log_after_remove
+ has_and_belongs_to_many :parrots_with_proc_callbacks, :class_name => "Parrot",
+ :before_add => proc {|p,pa| p.ship_log << "before_adding_proc_parrot_#{pa.id || '<new>'}"},
+ :after_add => proc {|p,pa| p.ship_log << "after_adding_proc_parrot_#{pa.id || '<new>'}"},
+ :before_remove => proc {|p,pa| p.ship_log << "before_removing_proc_parrot_#{pa.id}"},
+ :after_remove => proc {|p,pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}"}
+ has_many :treasures, :as => :looter
has_many :treasure_estimates, :through => :treasures, :source => :price_estimates
# These both have :autosave enabled because accepts_nested_attributes_for is used on them.
has_one :ship
has_many :birds
+ has_many :birds_with_method_callbacks, :class_name => "Bird",
+ :before_add => :log_before_add,
+ :after_add => :log_after_add,
+ :before_remove => :log_before_remove,
+ :after_remove => :log_after_remove
+ has_many :birds_with_proc_callbacks, :class_name => "Bird",
+ :before_add => proc {|p,b| p.ship_log << "before_adding_proc_bird_#{b.id || '<new>'}"},
+ :after_add => proc {|p,b| p.ship_log << "after_adding_proc_bird_#{b.id || '<new>'}"},
+ :before_remove => proc {|p,b| p.ship_log << "before_removing_proc_bird_#{b.id}"},
+ :after_remove => proc {|p,b| p.ship_log << "after_removing_proc_bird_#{b.id}"}
accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ accepts_nested_attributes_for :parrots_with_method_callbacks, :parrots_with_proc_callbacks,
+ :birds_with_method_callbacks, :birds_with_proc_callbacks, :allow_destroy => true
validates_presence_of :catchphrase
+
+ def ship_log
+ @ship_log ||= []
+ end
+
+ private
+ def log_before_add(record)
+ log(record, "before_adding_method")
+ end
+
+ def log_after_add(record)
+ log(record, "after_adding_method")
+ end
+
+ def log_before_remove(record)
+ log(record, "before_removing_method")
+ end
+
+ def log_after_remove(record)
+ log(record, "after_removing_method")
+ end
+
+ def log(record, callback)
+ ship_log << "#{callback}_#{record.class.name.downcase}_#{record.id || '<new>'}"
+ end
end
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 388fff8fba..374e536a5b 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -1,5 +1,7 @@
class Post < ActiveRecord::Base
named_scope :containing_the_letter_a, :conditions => "body LIKE '%a%'"
+ named_scope :ranked_by_comments, :order => "comments_count DESC"
+ named_scope :limit, lambda {|limit| {:limit => limit} }
named_scope :with_authors_at_address, lambda { |address| {
:conditions => [ 'authors.author_address_id = ?', address.id ],
:joins => 'JOIN authors ON authors.id = posts.author_id'
@@ -68,6 +70,10 @@ class Post < ActiveRecord::Base
:before_remove => lambda {|owner, reader| log(:removed, :before, reader.first_name) },
:after_remove => lambda {|owner, reader| log(:removed, :after, reader.first_name) }
+ def self.top(limit)
+ ranked_by_comments.limit(limit)
+ end
+
def self.reset_log
@log = []
end
diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb
index 812bc1f535..1c990acab6 100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
@@ -37,3 +37,9 @@ end
class SillyReply < Reply
belongs_to :reply, :foreign_key => "parent_id", :counter_cache => :replies_count
end
+
+module Web
+ class Reply < Web::Topic
+ belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true, :class_name => 'Web::Topic'
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 08bb24ed03..51012d22ed 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -1,7 +1,9 @@
class Topic < ActiveRecord::Base
named_scope :base
named_scope :written_before, lambda { |time|
- { :conditions => ['written_on < ?', time] }
+ if time
+ { :conditions => ['written_on < ?', time] }
+ end
}
named_scope :approved, :conditions => {:approved => true}
named_scope :rejected, :conditions => {:approved => false}
@@ -33,6 +35,8 @@ class Topic < ActiveRecord::Base
end
named_scope :named_extension, :extend => NamedExtension
named_scope :multiple_extensions, :extend => [MultipleExtensionTwo, MultipleExtensionOne]
+
+ named_scope :by_rejected_ids, lambda {{ :conditions => { :id => all(:conditions => {:approved => false}).map(&:id) } }}
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
serialize :content
@@ -69,3 +73,9 @@ class Topic < ActiveRecord::Base
end
end
end
+
+module Web
+ class Topic < ActiveRecord::Base
+ has_many :replies, :dependent => :destroy, :foreign_key => "parent_id", :class_name => 'Web::Reply'
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb
new file mode 100644
index 0000000000..79a88db0da
--- /dev/null
+++ b/activerecord/test/models/toy.rb
@@ -0,0 +1,4 @@
+class Toy < ActiveRecord::Base
+ set_primary_key :toy_id
+ belongs_to :pet
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 74a893983f..ea848a2940 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -155,6 +155,10 @@ ActiveRecord::Schema.define do
t.integer :course_id, :null => false
end
+ create_table :events, :force => true do |t|
+ t.string :title, :limit => 5
+ end
+
create_table :funny_jokes, :force => true do |t|
t.string :name
end
@@ -421,6 +425,11 @@ ActiveRecord::Schema.define do
t.column :taggings_count, :integer, :default => 0
end
+ create_table :toys, :primary_key => :toy_id ,:force => true do |t|
+ t.string :name
+ t.integer :pet_id, :integer
+ end
+
create_table :treasures, :force => true do |t|
t.column :name, :string
t.column :looter_id, :integer
diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG
index fcb5dd5f46..6572934893 100644
--- a/activeresource/CHANGELOG
+++ b/activeresource/CHANGELOG
@@ -1,6 +1,6 @@
-*2.3.0 [RC1] (February 1st, 2009)*
+*2.3.2 [Final] (March 15, 2009)*
-* Nothing new, just included in 2.3.0
+* Nothing new, just included in 2.3.2
*2.2.1 [RC2] (November 14th, 2008)*
diff --git a/activeresource/README b/activeresource/README
index 924017a659..127ac5b4a9 100644
--- a/activeresource/README
+++ b/activeresource/README
@@ -1,7 +1,7 @@
= Active Resource
Active Resource (ARes) connects business objects and Representational State Transfer (REST)
-web services. It implements object-relational mapping for REST webservices to provide transparent
+web services. It implements object-relational mapping for REST web services to provide transparent
proxying capabilities between a client (ActiveResource) and a RESTful service (which is provided by Simply RESTful routing
in ActionController::Resources).
@@ -22,14 +22,14 @@ received and serialized into a usable Ruby object.
=== Configuration and Usage
-Putting ActiveResource to use is very similar to ActiveRecord. It's as simple as creating a model class
+Putting Active Resource to use is very similar to Active Record. It's as simple as creating a model class
that inherits from ActiveResource::Base and providing a <tt>site</tt> class variable to it:
class Person < ActiveResource::Base
self.site = "http://api.people.com:3000/"
end
-Now the Person class is REST enabled and can invoke REST services very similarly to how ActiveRecord invokes
+Now the Person class is REST enabled and can invoke REST services very similarly to how Active Record invokes
lifecycle methods that operate against a persistent store.
# Find a person with id = 1
@@ -42,7 +42,7 @@ records. But rather than dealing directly with a database record, you're dealin
==== Protocol
Active Resource is built on a standard XML format for requesting and submitting resources over HTTP. It mirrors the RESTful routing
-built into ActionController but will also work with any other REST service that properly implements the protocol.
+built into Action Controller but will also work with any other REST service that properly implements the protocol.
REST uses HTTP, but unlike "typical" web applications, it makes use of all the verbs available in the HTTP specification:
* GET requests are used for finding and retrieving resources.
@@ -55,8 +55,8 @@ for more general information on REST web services, see the article here[http://e
==== Find
-GET Http requests expect the XML form of whatever resource/resources is/are being requested. So,
-for a request for a single element - the XML of that item is expected in response:
+Find requests use the GET method and expect the XML form of whatever resource/resources is/are being requested. So,
+for a request for a single element, the XML of that item is expected in response:
# Expects a response of
#
@@ -101,7 +101,7 @@ Collections can also be requested in a similar fashion
==== Create
-Creating a new resource submits the xml form of the resource as the body of the request and expects
+Creating a new resource submits the XML form of the resource as the body of the request and expects
a 'Location' header in the response with the RESTful URL location of the newly created resource. The
id of the newly created resource is parsed out of the Location response header and automatically set
as the id of the ARes object.
diff --git a/activeresource/Rakefile b/activeresource/Rakefile
index 6af2e6cfba..bf7bbb0201 100644
--- a/activeresource/Rakefile
+++ b/activeresource/Rakefile
@@ -67,7 +67,7 @@ spec = Gem::Specification.new do |s|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end
- s.add_dependency('activesupport', '= 2.3.0' + PKG_BUILD)
+ s.add_dependency('activesupport', '= 2.3.2' + PKG_BUILD)
s.require_path = 'lib'
s.autorequire = 'active_resource'
diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb
index a8c0da31f2..6cb5beb789 100644
--- a/activeresource/lib/active_resource/base.rb
+++ b/activeresource/lib/active_resource/base.rb
@@ -19,7 +19,7 @@ module ActiveResource
# 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 lifecycle 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
@@ -112,6 +112,7 @@ module ActiveResource
#
# 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
@@ -156,7 +157,7 @@ module ActiveResource
#
# === Validation errors
#
- # Active Resource supports validations on resources and will return errors if any these validations fail
+ # Active Resource supports validations on resources and will return errors if any of these validations fail
# (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.
@@ -413,7 +414,7 @@ module ActiveResource
# will split from the +prefix_options+.
#
# ==== Options
- # * +prefix_options+ - A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
+ # * +prefix_options+ - A hash to add a prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
# would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
# * +query_options+ - A hash to add items to the query string for the request.
#
@@ -691,7 +692,7 @@ module ActiveResource
end
- # A method to determine if the resource a \new object (i.e., it has not been POSTed to the remote service yet).
+ # Returns +true+ if this object hasn't yet been saved, otherwise, returns +false+.
#
# ==== Examples
# not_new = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall')
@@ -760,7 +761,7 @@ module ActiveResource
id.hash
end
- # Duplicate the current resource without saving it.
+ # Duplicates the current resource without saving it.
#
# ==== Examples
# my_invoice = Invoice.create(:customer => 'That Company')
@@ -779,8 +780,8 @@ module ActiveResource
end
end
- # 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
+ # Saves (+POST+) or \updates (+PUT+) a resource. Delegates to +create+ if the object is \new,
+ # +update+ if it exists. 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).
#
@@ -832,7 +833,7 @@ module ActiveResource
!new? && self.class.exists?(to_param, :params => prefix_options)
end
- # A method to convert the the resource to an XML string.
+ # Converts the resource to an XML string representation.
#
# ==== Options
# The +options+ parameter is handed off to the +to_xml+ method on each
@@ -861,8 +862,7 @@ module ActiveResource
attributes.to_xml({:root => self.class.element_name}.merge(options))
end
- # Returns a JSON string representing the model. Some configuration is
- # available through +options+.
+ # Converts the resource to a JSON string representation.
#
# ==== Options
# The +options+ are passed to the +to_json+ method on each
diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb
index 85103b53c5..80d5c95b68 100644
--- a/activeresource/lib/active_resource/connection.rb
+++ b/activeresource/lib/active_resource/connection.rb
@@ -95,46 +95,46 @@ module ActiveResource
@password = URI.decode(@site.password) if @site.password
end
- # Set user for remote service.
+ # Sets the user for remote service.
def user=(user)
@user = user
end
- # Set password for remote service.
+ # Sets the password for remote service.
def password=(password)
@password = password
end
- # Set the number of seconds after which HTTP requests to the remote service should time out.
+ # Sets the number of seconds after which HTTP requests to the remote service should time out.
def timeout=(timeout)
@timeout = timeout
end
- # Execute a GET request.
+ # Executes a GET request.
# Used to get (find) resources.
def get(path, headers = {})
format.decode(request(:get, path, build_request_headers(headers, :get)).body)
end
- # Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
+ # Executes a DELETE request (see HTTP protocol documentation if unfamiliar).
# Used to delete resources.
def delete(path, headers = {})
request(:delete, path, build_request_headers(headers, :delete))
end
- # Execute a PUT request (see HTTP protocol documentation if unfamiliar).
+ # Executes a PUT request (see HTTP protocol documentation if unfamiliar).
# Used to update resources.
def put(path, body = '', headers = {})
request(:put, path, body.to_s, build_request_headers(headers, :put))
end
- # Execute a POST request.
+ # Executes a POST request.
# Used to create new resources.
def post(path, body = '', headers = {})
request(:post, path, body.to_s, build_request_headers(headers, :post))
end
- # Execute a HEAD request.
+ # Executes a HEAD request.
# Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
def head(path, headers = {})
request(:head, path, build_request_headers(headers))
@@ -142,7 +142,7 @@ module ActiveResource
private
- # Makes request to remote service.
+ # Makes a request to the remote service.
def request(method, path, *arguments)
logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
result = nil
@@ -153,7 +153,7 @@ module ActiveResource
raise TimeoutError.new(e.message)
end
- # Handles response and error codes from remote service.
+ # Handles response and error codes from the remote service.
def handle_response(response)
case response.code.to_i
when 301,302
@@ -183,7 +183,7 @@ module ActiveResource
end
end
- # Creates new Net::HTTP instance for communication with
+ # Creates new Net::HTTP instance for communication with the
# remote service and resources.
def http
http = Net::HTTP.new(@site.host, @site.port)
diff --git a/activeresource/lib/active_resource/validations.rb b/activeresource/lib/active_resource/validations.rb
index de3339935f..8d21f8adbb 100644
--- a/activeresource/lib/active_resource/validations.rb
+++ b/activeresource/lib/active_resource/validations.rb
@@ -3,7 +3,7 @@ module ActiveResource
end
# Active Resource validation is reported to and from this object, which is used by Base#save
- # to determine whether the object in a valid state to be saved. See usage example in Validations.
+ # to determine whether the object is in a valid state to be saved. See usage example in Validations.
class Errors
include Enumerable
attr_reader :errors
@@ -14,7 +14,10 @@ module ActiveResource
@base, @errors = base, {}
end
- # Add an error to the base Active Resource object rather than an attribute.
+ # 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.
#
# ==== Examples
# my_folder = Folder.find(1)
@@ -68,9 +71,9 @@ module ActiveResource
!@errors[attribute.to_s].nil?
end
- # A method to return the errors associated with +attribute+, which returns nil, if no errors are
- # associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+,
- # or an array of error messages if more than one error is 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+.
#
# ==== Examples
# my_person = Person.new(params[:person])
@@ -92,9 +95,7 @@ module ActiveResource
alias :[] :on
- # A method to return errors assigned to +base+ object through add_to_base, which returns nil, if no errors are
- # associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+,
- # or an array of error messages if more than one error is associated with the specified +attribute+.
+ # Returns errors assigned to the base object through +add_to_base+ according to the normal rules of <tt>on(attribute)</tt>.
#
# ==== Examples
# my_account = Account.find(1)
diff --git a/activeresource/lib/active_resource/version.rb b/activeresource/lib/active_resource/version.rb
index c420ac813e..3df2555d53 100644
--- a/activeresource/lib/active_resource/version.rb
+++ b/activeresource/lib/active_resource/version.rb
@@ -2,7 +2,7 @@ module ActiveResource
module VERSION #:nodoc:
MAJOR = 2
MINOR = 3
- TINY = 0
+ TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end
diff --git a/activeresource/test/abstract_unit.rb b/activeresource/test/abstract_unit.rb
index 83bf1ed510..0f11ea482a 100644
--- a/activeresource/test/abstract_unit.rb
+++ b/activeresource/test/abstract_unit.rb
@@ -5,6 +5,7 @@ gem 'mocha', '>= 0.9.5'
require 'mocha'
$:.unshift "#{File.dirname(__FILE__)}/../lib"
+$:.unshift "#{File.dirname(__FILE__)}/../../activesupport/lib"
require 'active_resource'
require 'active_resource/http_mock'
diff --git a/activeresource/test/authorization_test.rb b/activeresource/test/authorization_test.rb
index ead7f5c12f..ca25f437e3 100644
--- a/activeresource/test/authorization_test.rb
+++ b/activeresource/test/authorization_test.rb
@@ -107,10 +107,10 @@ class AuthorizationTest < Test::Unit::TestCase
end
def test_raises_invalid_request_on_unauthorized_requests
- assert_raises(ActiveResource::InvalidRequestError) { @conn.post("/people/2.xml") }
- assert_raises(ActiveResource::InvalidRequestError) { @conn.post("/people/2/addresses.xml") }
- assert_raises(ActiveResource::InvalidRequestError) { @conn.put("/people/2.xml") }
- assert_raises(ActiveResource::InvalidRequestError) { @conn.delete("/people/2.xml") }
+ assert_raise(ActiveResource::InvalidRequestError) { @conn.post("/people/2.xml") }
+ assert_raise(ActiveResource::InvalidRequestError) { @conn.post("/people/2/addresses.xml") }
+ assert_raise(ActiveResource::InvalidRequestError) { @conn.put("/people/2.xml") }
+ assert_raise(ActiveResource::InvalidRequestError) { @conn.delete("/people/2.xml") }
end
protected
diff --git a/activeresource/test/base_test.rb b/activeresource/test/base_test.rb
index e22388f4a7..6ed6f1a406 100644
--- a/activeresource/test/base_test.rb
+++ b/activeresource/test/base_test.rb
@@ -47,7 +47,7 @@ class BaseTest < Test::Unit::TestCase
{:name => 'Milena',
:children => []}]}]}.to_xml(:root => 'customer')
# - resource with yaml array of strings; for ActiveRecords using serialize :bar, Array
- @marty = <<-eof
+ @marty = <<-eof.strip
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<person>
<id type=\"integer\">5</id>
@@ -564,14 +564,14 @@ class BaseTest < Test::Unit::TestCase
def test_custom_header
Person.headers['key'] = 'value'
- assert_raises(ActiveResource::ResourceNotFound) { Person.find(4) }
+ assert_raise(ActiveResource::ResourceNotFound) { Person.find(4) }
ensure
Person.headers.delete('key')
end
def test_find_by_id_not_found
- assert_raises(ActiveResource::ResourceNotFound) { Person.find(99) }
- assert_raises(ActiveResource::ResourceNotFound) { StreetAddress.find(1) }
+ assert_raise(ActiveResource::ResourceNotFound) { Person.find(99) }
+ assert_raise(ActiveResource::ResourceNotFound) { StreetAddress.find(1) }
end
def test_find_all_by_from
@@ -689,7 +689,7 @@ class BaseTest < Test::Unit::TestCase
ActiveResource::HttpMock.respond_to do |mock|
mock.post "/people.xml", {}, nil, 409
end
- assert_raises(ActiveResource::ResourceConflict) { Person.create(:name => 'Rick') }
+ assert_raise(ActiveResource::ResourceConflict) { Person.create(:name => 'Rick') }
end
def test_create_without_location
@@ -726,7 +726,7 @@ class BaseTest < Test::Unit::TestCase
matz.non_ar_arr = ["not", "ARes"]
matz_c = matz.clone
assert matz_c.new?
- assert_raises(NoMethodError) {matz_c.address}
+ assert_raise(NoMethodError) {matz_c.address}
assert_equal matz.non_ar_hash, matz_c.non_ar_hash
assert_equal matz.non_ar_arr, matz_c.non_ar_arr
@@ -764,7 +764,7 @@ class BaseTest < Test::Unit::TestCase
mock.get "/people/2.xml", {}, @david
mock.put "/people/2.xml", @default_request_headers, nil, 409
end
- assert_raises(ActiveResource::ResourceConflict) { Person.find(2).save }
+ assert_raise(ActiveResource::ResourceConflict) { Person.find(2).save }
end
def test_destroy
@@ -772,7 +772,7 @@ class BaseTest < Test::Unit::TestCase
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/1.xml", {}, nil, 404
end
- assert_raises(ActiveResource::ResourceNotFound) { Person.find(1).destroy }
+ assert_raise(ActiveResource::ResourceNotFound) { Person.find(1).destroy }
end
def test_destroy_with_custom_prefix
@@ -780,7 +780,7 @@ class BaseTest < Test::Unit::TestCase
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/1/addresses/1.xml", {}, nil, 404
end
- assert_raises(ActiveResource::ResourceNotFound) { StreetAddress.find(1, :params => { :person_id => 1 }) }
+ assert_raise(ActiveResource::ResourceNotFound) { StreetAddress.find(1, :params => { :person_id => 1 }) }
end
def test_delete
@@ -788,7 +788,7 @@ class BaseTest < Test::Unit::TestCase
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/1.xml", {}, nil, 404
end
- assert_raises(ActiveResource::ResourceNotFound) { Person.find(1) }
+ assert_raise(ActiveResource::ResourceNotFound) { Person.find(1) }
end
def test_delete_with_custom_prefix
@@ -796,7 +796,7 @@ class BaseTest < Test::Unit::TestCase
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/people/1/addresses/1.xml", {}, nil, 404
end
- assert_raises(ActiveResource::ResourceNotFound) { StreetAddress.find(1, :params => { :person_id => 1 }) }
+ assert_raise(ActiveResource::ResourceNotFound) { StreetAddress.find(1, :params => { :person_id => 1 }) }
end
def test_exists
diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG
index 70bea27cd1..ab40e1a17a 100644
--- a/activesupport/CHANGELOG
+++ b/activesupport/CHANGELOG
@@ -1,4 +1,9 @@
-*Edge*
+*2.3.2 [Final] (March 15, 2009)*
+
+* XmlMini supports LibXML and Nokogiri backends. #2084, #2190 [Bart ten Brinke, Aaron Patterson]
+ Example: XmlMini.backend = 'Nokogiri'
+
+* Vendorize i18n 0.1.3 gem (fixes issues with incompatible character encodings in Ruby 1.9) #2038 [Akira Matsuda]
* Update bundled memcache-client from 1.5.0.5 to 1.6.4.99. See http://www.mikeperham.com/2009/02/15/memcache-client-performance/ [Mike Perham]
@@ -10,9 +15,6 @@
* Introduce Array.wrap(foo) to wrap the argument in an array unless it's already an array. Wraps nil as an empty array. Use instead of Array(foo) and foo.to_a since they treat String as Enumerable. [Jeremy Kemper]
-
-*2.3.0 [RC1] (February 1st, 2009)*
-
* TimeWithZone#xmlschema accepts optional fraction_digits argument [#1725 state:resolved] [Nicholas Dainty]
* Object#tap shim for Ruby < 1.8.7. Similar to Object#returning, tap yields self then returns self. [Jeremy Kemper]
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
index a9b50ca92d..ba8e022fb2 100644
--- a/activesupport/lib/active_support/core_ext/array/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -7,10 +7,9 @@ module ActiveSupport #:nodoc:
# * <tt>:two_words_connector</tt> - The sign or word used to join the elements in arrays with two elements (default: " and ")
# * <tt>:last_word_connector</tt> - The sign or word used to join the last element in arrays with three or more elements (default: ", and ")
def to_sentence(options = {})
-
- default_words_connector = I18n.translate(:'support.array.words_connector', :locale => options[:locale])
+ default_words_connector = I18n.translate(:'support.array.words_connector', :locale => options[:locale])
default_two_words_connector = I18n.translate(:'support.array.two_words_connector', :locale => options[:locale])
- default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale])
+ default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale])
# Try to emulate to_senteces previous to 2.3
if options.has_key?(:connector) || options.has_key?(:skip_last_comma)
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
index 991a5a6a89..10435975c5 100644
--- a/activesupport/lib/active_support/core_ext/hash/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -24,11 +24,12 @@ module ActiveSupport #:nodoc:
"Bignum" => "integer",
"BigDecimal" => "decimal",
"Float" => "float",
+ "TrueClass" => "boolean",
+ "FalseClass" => "boolean",
"Date" => "date",
"DateTime" => "datetime",
"Time" => "datetime",
- "TrueClass" => "boolean",
- "FalseClass" => "boolean"
+ "ActiveSupport::TimeWithZone" => "datetime"
} unless defined?(XML_TYPE_NAMES)
XML_FORMATTING = {
diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
index c96c5160b3..34ba8a005d 100644
--- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
+++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
@@ -91,6 +91,12 @@ class HashWithIndifferentAccess < Hash
self.dup.update(hash)
end
+ # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
+ # This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess.
+ def reverse_merge(other_hash)
+ super other_hash.with_indifferent_access
+ end
+
# Removes a specified key from the hash.
def delete(key)
super(convert_key(key))
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index 15ad3d99cc..48e812aaf8 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -102,8 +102,8 @@ module ActiveSupport #:nodoc:
#
# <%= link_to(@person.name, person_path %>
# # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
- def parameterize
- Inflector.parameterize(self)
+ def parameterize(sep = '-')
+ Inflector.parameterize(self, sep)
end
# Creates the name of a table like Rails does for models to table names. This method
diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb
index c41e86dfd1..f64661c5b1 100644
--- a/activesupport/lib/active_support/duration.rb
+++ b/activesupport/lib/active_support/duration.rb
@@ -70,7 +70,7 @@ module ActiveSupport
[:years, :months, :days, :minutes, :seconds].map do |length|
n = consolidated[length]
"#{n} #{n == 1 ? length.to_s.singularize : length.to_s}" if n.nonzero?
- end.compact.to_sentence
+ end.compact.to_sentence(:locale => :en)
end
protected
diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb
index 5ff6f50fb3..3ed30bdf56 100644
--- a/activesupport/lib/active_support/inflector.rb
+++ b/activesupport/lib/active_support/inflector.rb
@@ -257,15 +257,17 @@ module ActiveSupport
# <%= link_to(@person.name, person_path(@person)) %>
# # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
def parameterize(string, sep = '-')
- re_sep = Regexp.escape(sep)
# replace accented chars with ther ascii equivalents
parameterized_string = transliterate(string)
# Turn unwanted chars into the seperator
parameterized_string.gsub!(/[^a-z0-9\-_\+]+/i, sep)
- # No more than one of the separator in a row.
- parameterized_string.squeeze!(sep)
- # Remove leading/trailing separator.
- parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '')
+ unless sep.blank?
+ re_sep = Regexp.escape(sep)
+ # No more than one of the separator in a row.
+ parameterized_string.gsub!(/#{re_sep}{2,}/, sep)
+ # Remove leading/trailing separator.
+ parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '')
+ end
parameterized_string.downcase
end
diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb
index 5eb8c0fd7d..0e079341ff 100644
--- a/activesupport/lib/active_support/json/decoding.rb
+++ b/activesupport/lib/active_support/json/decoding.rb
@@ -43,14 +43,32 @@ module ActiveSupport
end
if marks.empty?
- json.gsub(/\\\//, '/')
+ json.gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do
+ ustr = $1
+ if ustr.starts_with?('u')
+ [ustr[1..-1].to_i(16)].pack("U")
+ elsif ustr == '\\'
+ '\\\\'
+ else
+ ustr
+ end
+ end
else
left_pos = [-1].push(*marks)
right_pos = marks << scanner.pos + scanner.rest_size
output = []
left_pos.each_with_index do |left, i|
scanner.pos = left.succ
- output << scanner.peek(right_pos[i] - scanner.pos + 1)
+ output << scanner.peek(right_pos[i] - scanner.pos + 1).gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do
+ ustr = $1
+ if ustr.starts_with?('u')
+ [ustr[1..-1].to_i(16)].pack("U")
+ elsif ustr == '\\'
+ '\\\\'
+ else
+ ustr
+ end
+ end
end
output = output * " "
diff --git a/activesupport/lib/active_support/memoizable.rb b/activesupport/lib/active_support/memoizable.rb
index 952b4d8063..71cfe61739 100644
--- a/activesupport/lib/active_support/memoizable.rb
+++ b/activesupport/lib/active_support/memoizable.rb
@@ -89,6 +89,10 @@ module ActiveSupport
end # end
end # end
end # end
+ #
+ if private_method_defined?(#{original_method.inspect}) # if private_method_defined?(:_unmemoized_mime_type)
+ private #{symbol.inspect} # private :mime_type
+ end # end
EOS
end
end
diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb
index 62b6d798ef..60f082bcc1 100644
--- a/activesupport/lib/active_support/multibyte/chars.rb
+++ b/activesupport/lib/active_support/multibyte/chars.rb
@@ -344,7 +344,19 @@ module ActiveSupport #:nodoc:
end
alias_method :[], :slice
- # Converts first character in the string to Unicode value
+ # Like <tt>String#slice!</tt>, except instead of byte offsets you specify character offsets.
+ #
+ # Example:
+ # s = 'こんにちは'
+ # s.mb_chars.slice!(2..3).to_s #=> "にち"
+ # s #=> "こんは"
+ def slice!(*args)
+ slice = self[*args]
+ self[*args] = ''
+ slice
+ end
+
+ # Returns the codepoint of the first character in the string.
#
# Example:
# 'こんにちは'.mb_chars.ord #=> 12371
@@ -432,7 +444,7 @@ module ActiveSupport #:nodoc:
chars(self.class.tidy_bytes(@wrapped_string))
end
- %w(lstrip rstrip strip reverse upcase downcase slice tidy_bytes capitalize).each do |method|
+ %w(lstrip rstrip strip reverse upcase downcase tidy_bytes capitalize).each do |method|
define_method("#{method}!") do |*args|
unless args.nil?
@wrapped_string = send(method, *args).to_s
diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb
index 66aab9e562..fed8094a24 100644
--- a/activesupport/lib/active_support/ordered_hash.rb
+++ b/activesupport/lib/active_support/ordered_hash.rb
@@ -54,7 +54,7 @@ module ActiveSupport
end
def to_hash
- Hash.new(self)
+ self
end
def each_key
@@ -93,7 +93,7 @@ module ActiveSupport
end
def inspect
- "#<OrderedHash #{self.to_hash.inspect}>"
+ "#<OrderedHash #{super}>"
end
private
diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb
index 97b2b6ef9c..f05d4098fc 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -20,7 +20,7 @@ module ActiveSupport
alias_method :method_name, :name
else
# TODO: Figure out how to get the Rails::BacktraceFilter into minitest/unit
- if defined?(Rails)
+ if defined?(Rails) && ENV['BACKTRACE'].nil?
require 'rails/backtrace_cleaner'
Test::Unit::Util::BacktraceFilter.module_eval { include Rails::BacktraceFilterForTestUnit }
end
diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb
index 7edf6fdb32..aaf9f8f42c 100644
--- a/activesupport/lib/active_support/testing/setup_and_teardown.rb
+++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb
@@ -45,7 +45,12 @@ module ActiveSupport
return if @method_name.to_s == "default_test"
if using_mocha = respond_to?(:mocha_verify)
- assertion_counter = Mocha::TestCaseAdapter::AssertionCounter.new(result)
+ assertion_counter_klass = if defined?(Mocha::TestCaseAdapter::AssertionCounter)
+ Mocha::TestCaseAdapter::AssertionCounter
+ else
+ Mocha::Integration::TestUnit::AssertionCounter
+ end
+ assertion_counter = assertion_counter_klass.new(result)
end
yield(Test::Unit::TestCase::STARTED, name)
diff --git a/activesupport/lib/active_support/vendor.rb b/activesupport/lib/active_support/vendor.rb
index 39da70a9f3..28852e65c8 100644
--- a/activesupport/lib/active_support/vendor.rb
+++ b/activesupport/lib/active_support/vendor.rb
@@ -22,8 +22,8 @@ end
# TODO I18n gem has not been released yet
# begin
-# gem 'i18n', '~> 0.1.1'
+# gem 'i18n', '~> 0.1.3'
# rescue Gem::LoadError
- $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.1.1/lib"
+ $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.1.3/lib"
require 'i18n'
# end
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore b/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore
deleted file mode 100644
index 0f41a39f89..0000000000
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-.DS_Store
-test/rails/fixtures
-doc
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/MIT-LICENSE b/activesupport/lib/active_support/vendor/i18n-0.1.3/MIT-LICENSE
index ed8e9ee66d..ed8e9ee66d 100755
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/MIT-LICENSE
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/MIT-LICENSE
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/README.textile b/activesupport/lib/active_support/vendor/i18n-0.1.3/README.textile
index a07fc8426d..a07fc8426d 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/README.textile
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/README.textile
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/Rakefile b/activesupport/lib/active_support/vendor/i18n-0.1.3/Rakefile
index 2164e13e69..2164e13e69 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/Rakefile
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/Rakefile
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec b/activesupport/lib/active_support/vendor/i18n-0.1.3/i18n.gemspec
index 14294606bd..f102689a6f 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/i18n.gemspec
@@ -1,7 +1,7 @@
Gem::Specification.new do |s|
s.name = "i18n"
- s.version = "0.1.1"
- s.date = "2008-10-26"
+ s.version = "0.1.3"
+ s.date = "2009-01-09"
s.summary = "Internationalization support for Ruby"
s.email = "rails-i18n@googlegroups.com"
s.homepage = "http://rails-i18n.org"
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n.rb
index 76361bed90..76361bed90 100755
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n.rb
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/backend/simple.rb
index b54164d496..c09acd7d2d 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/backend/simple.rb
@@ -151,12 +151,7 @@ module I18n
def interpolate(locale, string, values = {})
return string unless string.is_a?(String)
- if string.respond_to?(:force_encoding)
- original_encoding = string.encoding
- string.force_encoding(Encoding::BINARY)
- end
-
- result = string.gsub(MATCH) do
+ string.gsub(MATCH) do
escaped, pattern, key = $1, $2, $2.to_sym
if escaped
@@ -169,9 +164,6 @@ module I18n
values[key].to_s
end
end
-
- result.force_encoding(original_encoding) if original_encoding
- result
end
# Loads a single translations file by delegating to #load_rb or
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/exceptions.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/exceptions.rb
index b5cea7acb4..b5cea7acb4 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/exceptions.rb
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/exceptions.rb
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/all.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/all.rb
index 353712da49..353712da49 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/all.rb
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/all.rb
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_exceptions_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_exceptions_test.rb
index dfcba6901f..dfcba6901f 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_exceptions_test.rb
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_exceptions_test.rb
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_test.rb
index bbb35ec809..50d6832c9e 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_test.rb
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_test.rb
@@ -116,10 +116,10 @@ class I18nTest < Test::Unit::TestCase
end
def test_localize_nil_raises_argument_error
- assert_raises(I18n::ArgumentError) { I18n.l nil }
+ assert_raise(I18n::ArgumentError) { I18n.l nil }
end
def test_localize_object_raises_argument_error
- assert_raises(I18n::ArgumentError) { I18n.l Object.new }
+ assert_raise(I18n::ArgumentError) { I18n.l Object.new }
end
end
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.rb
index 6044ce10d9..6044ce10d9 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.rb
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.rb
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.yml b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.yml
index 0b298c9c0e..0b298c9c0e 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.yml
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.yml
diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/simple_backend_test.rb
index 8ba7036abf..65f3ac11a6 100644
--- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb
+++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/simple_backend_test.rb
@@ -155,7 +155,7 @@ class I18nSimpleBackendTranslateTest < Test::Unit::TestCase
end
def test_translate_given_an_array_of_inexistent_keys_it_raises_missing_translation_data
- assert_raises I18n::MissingTranslationData do
+ assert_raise I18n::MissingTranslationData do
@backend.translate('en', :does_not_exist, :scope => [:foo], :default => [:does_not_exist_2, :does_not_exist_3])
end
end
@@ -180,11 +180,11 @@ class I18nSimpleBackendTranslateTest < Test::Unit::TestCase
end
def test_translate_given_nil_as_a_locale_raises_an_argument_error
- assert_raises(I18n::InvalidLocale){ @backend.translate nil, :bar }
+ assert_raise(I18n::InvalidLocale){ @backend.translate nil, :bar }
end
def test_translate_with_a_bogus_key_and_no_default_raises_missing_translation_data
- assert_raises(I18n::MissingTranslationData){ @backend.translate 'de', :bogus }
+ assert_raise(I18n::MissingTranslationData){ @backend.translate 'de', :bogus }
end
end
@@ -230,15 +230,15 @@ class I18nSimpleBackendPluralizeTest < Test::Unit::TestCase
end
def test_interpolate_given_incomplete_pluralization_data_raises_invalid_pluralization_data
- assert_raises(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, {:one => 'bar'}, 2) }
+ assert_raise(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, {:one => 'bar'}, 2) }
end
# def test_interpolate_given_a_string_raises_invalid_pluralization_data
- # assert_raises(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, 'bar', 2) }
+ # assert_raise(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, 'bar', 2) }
# end
#
# def test_interpolate_given_an_array_raises_invalid_pluralization_data
- # assert_raises(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, ['bar'], 2) }
+ # assert_raise(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, ['bar'], 2) }
# end
end
@@ -253,6 +253,32 @@ class I18nSimpleBackendInterpolateTest < Test::Unit::TestCase
assert_equal 'Häi David!', @backend.send(:interpolate, nil, 'Häi {{name}}!', :name => 'David')
end
+ def test_interpolate_given_an_unicode_value_hash_interpolates_to_the_string
+ assert_equal 'Hi ゆきひろ!', @backend.send(:interpolate, nil, 'Hi {{name}}!', :name => 'ゆきひろ')
+ end
+
+ def test_interpolate_given_an_unicode_value_hash_interpolates_into_unicode_string
+ assert_equal 'こんにちは、ゆきひろさん!', @backend.send(:interpolate, nil, 'こんにちは、{{name}}さん!', :name => 'ゆきひろ')
+ end
+
+ if Kernel.const_defined?(:Encoding)
+ def test_interpolate_given_a_non_unicode_multibyte_value_hash_interpolates_into_a_string_with_the_same_encoding
+ assert_equal euc_jp('Hi ゆきひろ!'), @backend.send(:interpolate, nil, 'Hi {{name}}!', :name => euc_jp('ゆきひろ'))
+ end
+
+ def test_interpolate_given_an_unicode_value_hash_into_a_non_unicode_multibyte_string_raises_encoding_compatibility_error
+ assert_raise(Encoding::CompatibilityError) do
+ @backend.send(:interpolate, nil, euc_jp('こんにちは、{{name}}さん!'), :name => 'ゆきひろ')
+ end
+ end
+
+ def test_interpolate_given_a_non_unicode_multibyte_value_hash_into_an_unicode_string_raises_encoding_compatibility_error
+ assert_raise(Encoding::CompatibilityError) do
+ @backend.send(:interpolate, nil, 'こんにちは、{{name}}さん!', :name => euc_jp('ゆきひろ'))
+ end
+ end
+ end
+
def test_interpolate_given_nil_as_a_string_returns_nil
assert_nil @backend.send(:interpolate, nil, nil, :name => 'David')
end
@@ -266,11 +292,17 @@ class I18nSimpleBackendInterpolateTest < Test::Unit::TestCase
end
def test_interpolate_given_an_empty_values_hash_raises_missing_interpolation_argument
- assert_raises(I18n::MissingInterpolationArgument) { @backend.send(:interpolate, nil, 'Hi {{name}}!', {}) }
+ assert_raise(I18n::MissingInterpolationArgument) { @backend.send(:interpolate, nil, 'Hi {{name}}!', {}) }
end
def test_interpolate_given_a_string_containing_a_reserved_key_raises_reserved_interpolation_key
- assert_raises(I18n::ReservedInterpolationKey) { @backend.send(:interpolate, nil, '{{default}}', {:default => nil}) }
+ assert_raise(I18n::ReservedInterpolationKey) { @backend.send(:interpolate, nil, '{{default}}', {:default => nil}) }
+ end
+
+ private
+
+ def euc_jp(string)
+ string.encode!(Encoding::EUC_JP)
end
end
@@ -320,11 +352,11 @@ class I18nSimpleBackendLocalizeDateTest < Test::Unit::TestCase
end
def test_localize_nil_raises_argument_error
- assert_raises(I18n::ArgumentError) { @backend.localize 'de', nil }
+ assert_raise(I18n::ArgumentError) { @backend.localize 'de', nil }
end
def test_localize_object_raises_argument_error
- assert_raises(I18n::ArgumentError) { @backend.localize 'de', Object.new }
+ assert_raise(I18n::ArgumentError) { @backend.localize 'de', Object.new }
end
end
@@ -454,7 +486,7 @@ class I18nSimpleBackendLoadTranslationsTest < Test::Unit::TestCase
include I18nSimpleBackendTestSetup
def test_load_translations_with_unknown_file_type_raises_exception
- assert_raises(I18n::UnknownFileType) { @backend.load_translations "#{@locale_dir}/en.xml" }
+ assert_raise(I18n::UnknownFileType) { @backend.load_translations "#{@locale_dir}/en.xml" }
end
def test_load_translations_with_ruby_file_type_does_not_raise_exception
@@ -485,6 +517,10 @@ end
class I18nSimpleBackendLoadPathTest < Test::Unit::TestCase
include I18nSimpleBackendTestSetup
+ def teardown
+ I18n.load_path = []
+ end
+
def test_nested_load_paths_do_not_break_locale_loading
@backend = I18n::Backend::Simple.new
I18n.load_path = [[File.dirname(__FILE__) + '/locale/en.yml']]
@@ -492,6 +528,14 @@ class I18nSimpleBackendLoadPathTest < Test::Unit::TestCase
assert_nothing_raised { @backend.send :init_translations }
assert_not_nil backend_get_translations
end
+
+ def test_adding_arrays_of_filenames_to_load_path_do_not_break_locale_loading
+ @backend = I18n::Backend::Simple.new
+ I18n.load_path << Dir[File.dirname(__FILE__) + '/locale/*.{rb,yml}']
+ assert_nil backend_get_translations
+ assert_nothing_raised { @backend.send :init_translations }
+ assert_not_nil backend_get_translations
+ end
end
class I18nSimpleBackendReloadTranslationsTest < Test::Unit::TestCase
@@ -521,4 +565,4 @@ class I18nSimpleBackendReloadTranslationsTest < Test::Unit::TestCase
@backend.reload!
assert_equal @backend.initialized?, false
end
-end \ No newline at end of file
+end
diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb
index 3e2b29b327..30f598a8de 100644
--- a/activesupport/lib/active_support/version.rb
+++ b/activesupport/lib/active_support/version.rb
@@ -2,7 +2,7 @@ module ActiveSupport
module VERSION #:nodoc:
MAJOR = 2
MINOR = 3
- TINY = 0
+ TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end
diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index ce3f50620d..ccd1349491 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -1,113 +1,31 @@
-# = XmlMini
-# This is a derivitive work of XmlSimple 1.0.11
-# Author:: Joseph Holsten <joseph@josephholsten.com>
-# Copyright:: Copyright (c) 2008 Joseph Holsten
-# Copyright:: Copyright (c) 2003-2006 Maik Schmidt <contact@maik-schmidt.de>
-# License:: Distributes under the same terms as Ruby.
module ActiveSupport
+ # = XmlMini
+ #
+ # To use the much faster libxml parser:
+ # gem 'libxml-ruby', '=0.9.7'
+ # XmlMini.backend = 'LibXML'
module XmlMini
extend self
- CONTENT_KEY = '__content__'.freeze
+ attr_reader :backend
+ delegate :parse, :to => :backend
- # Parse an XML Document string into a simple hash
- #
- # Same as XmlSimple::xml_in but doesn't shoot itself in the foot,
- # and uses the defaults from ActiveSupport
- #
- # string::
- # XML Document string to parse
- def parse(string)
- require 'rexml/document' unless defined?(REXML::Document)
- doc = REXML::Document.new(string)
- merge_element!({}, doc.root)
- end
-
- private
- # Convert an XML element and merge into the hash
- #
- # hash::
- # Hash to merge the converted element into.
- # element::
- # XML element to merge into hash
- def merge_element!(hash, element)
- merge!(hash, element.name, collapse(element))
- end
-
- # Actually converts an XML document element into a data structure.
- #
- # element::
- # The document element to be collapsed.
- def collapse(element)
- hash = get_attributes(element)
-
- if element.has_elements?
- element.each_element {|child| merge_element!(hash, child) }
- merge_texts!(hash, element) unless empty_content?(element)
- hash
- else
- merge_texts!(hash, element)
- end
- end
-
- # Merge all the texts of an element into the hash
- #
- # hash::
- # Hash to add the converted emement to.
- # element::
- # XML element whose texts are to me merged into the hash
- def merge_texts!(hash, element)
- unless element.has_text?
- hash
- else
- # must use value to prevent double-escaping
- merge!(hash, CONTENT_KEY, element.texts.sum(&:value))
- end
- end
-
- # Adds a new key/value pair to an existing Hash. If the key to be added
- # already exists and the existing value associated with key is not
- # an Array, it will be wrapped in an Array. Then the new value is
- # appended to that Array.
- #
- # hash::
- # Hash to add key/value pair to.
- # key::
- # Key to be added.
- # value::
- # Value to be associated with key.
- def merge!(hash, key, value)
- if hash.has_key?(key)
- if hash[key].instance_of?(Array)
- hash[key] << value
- else
- hash[key] = [hash[key], value]
- end
- elsif value.instance_of?(Array)
- hash[key] = [value]
- else
- hash[key] = value
- end
- hash
- end
-
- # Converts the attributes array of an XML element into a hash.
- # Returns an empty Hash if node has no attributes.
- #
- # element::
- # XML element to extract attributes from.
- def get_attributes(element)
- attributes = {}
- element.attributes.each { |n,v| attributes[n] = v }
- attributes
+ def backend=(name)
+ if name.is_a?(Module)
+ @backend = name
+ else
+ require "active_support/xml_mini/#{name.to_s.downcase}.rb"
+ @backend = ActiveSupport.const_get("XmlMini_#{name}")
end
+ end
- # Determines if a document element has text content
- #
- # element::
- # XML element to be checked.
- def empty_content?(element)
- element.texts.join.blank?
- end
+ def with_backend(name)
+ old_backend, self.backend = backend, name
+ yield
+ ensure
+ self.backend = old_backend
+ end
end
-end \ No newline at end of file
+
+ XmlMini.backend = 'REXML'
+end
diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb
new file mode 100644
index 0000000000..3586b24a6b
--- /dev/null
+++ b/activesupport/lib/active_support/xml_mini/libxml.rb
@@ -0,0 +1,133 @@
+require 'libxml'
+
+# = XmlMini LibXML implementation
+module ActiveSupport
+ module XmlMini_LibXML #:nodoc:
+ extend self
+
+ # Parse an XML Document string into a simple hash using libxml.
+ # string::
+ # XML Document string to parse
+ def parse(string)
+ LibXML::XML.default_keep_blanks = false
+
+ if string.blank?
+ {}
+ else
+ LibXML::XML::Parser.string(string.strip).parse.to_hash
+ end
+ end
+
+ end
+end
+
+module LibXML
+ module Conversions
+ module Document
+ def to_hash
+ root.to_hash
+ end
+ end
+
+ module Node
+ CONTENT_ROOT = '__content__'
+ LIB_XML_LIMIT = 30000000 # Hardcoded LibXML limit
+
+ # Convert XML document to hash
+ #
+ # hash::
+ # Hash to merge the converted element into.
+ def to_hash(hash={})
+ if text?
+ raise LibXML::XML::Error if content.length >= LIB_XML_LIMIT
+ hash[CONTENT_ROOT] = content
+ else
+ sub_hash = insert_name_into_hash(hash, name)
+ attributes_to_hash(sub_hash)
+ if array?
+ children_array_to_hash(sub_hash)
+ elsif yaml?
+ children_yaml_to_hash(sub_hash)
+ else
+ children_to_hash(sub_hash)
+ end
+ end
+ hash
+ end
+
+ protected
+
+ # Insert name into hash
+ #
+ # hash::
+ # Hash to merge the converted element into.
+ # name::
+ # name to to merge into hash
+ def insert_name_into_hash(hash, name)
+ sub_hash = {}
+ if hash[name]
+ if !hash[name].kind_of? Array
+ hash[name] = [hash[name]]
+ end
+ hash[name] << sub_hash
+ else
+ hash[name] = sub_hash
+ end
+ sub_hash
+ end
+
+ # Insert children into hash
+ #
+ # hash::
+ # Hash to merge the children into.
+ def children_to_hash(hash={})
+ each { |child| child.to_hash(hash) }
+ attributes_to_hash(hash)
+ hash
+ end
+
+ # Convert xml attributes to hash
+ #
+ # hash::
+ # Hash to merge the attributes into
+ def attributes_to_hash(hash={})
+ each_attr { |attr| hash[attr.name] = attr.value }
+ hash
+ end
+
+ # Convert array into hash
+ #
+ # hash::
+ # Hash to merge the array into
+ def children_array_to_hash(hash={})
+ hash[child.name] = map do |child|
+ returning({}) { |sub_hash| child.children_to_hash(sub_hash) }
+ end
+ hash
+ end
+
+ # Convert yaml into hash
+ #
+ # hash::
+ # Hash to merge the yaml into
+ def children_yaml_to_hash(hash = {})
+ hash[CONTENT_ROOT] = content unless content.blank?
+ hash
+ end
+
+ # Check if child is of type array
+ def array?
+ child? && child.next? && child.name == child.next.name
+ end
+
+ # Check if child is of type yaml
+ def yaml?
+ attributes.collect{|x| x.value}.include?('yaml')
+ end
+
+ end
+ end
+end
+
+LibXML::XML::Document.send(:include, LibXML::Conversions::Document)
+LibXML::XML::Node.send(:include, LibXML::Conversions::Node)
diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb
new file mode 100644
index 0000000000..10281584fc
--- /dev/null
+++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb
@@ -0,0 +1,77 @@
+require 'nokogiri'
+
+# = XmlMini Nokogiri implementation
+module ActiveSupport
+ module XmlMini_Nokogiri #:nodoc:
+ extend self
+
+ # Parse an XML Document string into a simple hash using libxml / nokogiri.
+ # string::
+ # XML Document string to parse
+ def parse(string)
+ if string.blank?
+ {}
+ else
+ doc = Nokogiri::XML(string)
+ raise doc.errors.first if doc.errors.length > 0
+ doc.to_hash
+ end
+ end
+
+ module Conversions
+ module Document
+ def to_hash
+ root.to_hash
+ end
+ end
+
+ module Node
+ CONTENT_ROOT = '__content__'
+
+ # Convert XML document to hash
+ #
+ # hash::
+ # Hash to merge the converted element into.
+ def to_hash(hash = {})
+ hash[name] ||= attributes_as_hash
+
+ walker = lambda { |memo, parent, child, callback|
+ next if child.blank? && 'file' != parent['type']
+
+ if child.text?
+ (memo[CONTENT_ROOT] ||= '') << child.content
+ next
+ end
+
+ name = child.name
+
+ child_hash = child.attributes_as_hash
+ if memo[name]
+ memo[name] = [memo[name]].flatten
+ memo[name] << child_hash
+ else
+ memo[name] = child_hash
+ end
+
+ # Recusively walk children
+ child.children.each { |c|
+ callback.call(child_hash, child, c, callback)
+ }
+ }
+
+ children.each { |c| walker.call(hash[name], self, c, walker) }
+ hash
+ end
+
+ def attributes_as_hash
+ Hash[*(attribute_nodes.map { |node|
+ [node.node_name, node.value]
+ }.flatten)]
+ end
+ end
+ end
+
+ Nokogiri::XML::Document.send(:include, Conversions::Document)
+ Nokogiri::XML::Node.send(:include, Conversions::Node)
+ end
+end
diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb
new file mode 100644
index 0000000000..a8fdeca967
--- /dev/null
+++ b/activesupport/lib/active_support/xml_mini/rexml.rb
@@ -0,0 +1,108 @@
+# = XmlMini ReXML implementation
+module ActiveSupport
+ module XmlMini_REXML #:nodoc:
+ extend self
+
+ CONTENT_KEY = '__content__'.freeze
+
+ # Parse an XML Document string into a simple hash
+ #
+ # Same as XmlSimple::xml_in but doesn't shoot itself in the foot,
+ # and uses the defaults from ActiveSupport
+ #
+ # string::
+ # XML Document string to parse
+ def parse(string)
+ require 'rexml/document' unless defined?(REXML::Document)
+ doc = REXML::Document.new(string)
+ merge_element!({}, doc.root)
+ end
+
+ private
+ # Convert an XML element and merge into the hash
+ #
+ # hash::
+ # Hash to merge the converted element into.
+ # element::
+ # XML element to merge into hash
+ def merge_element!(hash, element)
+ merge!(hash, element.name, collapse(element))
+ end
+
+ # Actually converts an XML document element into a data structure.
+ #
+ # element::
+ # The document element to be collapsed.
+ def collapse(element)
+ hash = get_attributes(element)
+
+ if element.has_elements?
+ element.each_element {|child| merge_element!(hash, child) }
+ merge_texts!(hash, element) unless empty_content?(element)
+ hash
+ else
+ merge_texts!(hash, element)
+ end
+ end
+
+ # Merge all the texts of an element into the hash
+ #
+ # hash::
+ # Hash to add the converted emement to.
+ # element::
+ # XML element whose texts are to me merged into the hash
+ def merge_texts!(hash, element)
+ unless element.has_text?
+ hash
+ else
+ # must use value to prevent double-escaping
+ merge!(hash, CONTENT_KEY, element.texts.sum(&:value))
+ end
+ end
+
+ # Adds a new key/value pair to an existing Hash. If the key to be added
+ # already exists and the existing value associated with key is not
+ # an Array, it will be wrapped in an Array. Then the new value is
+ # appended to that Array.
+ #
+ # hash::
+ # Hash to add key/value pair to.
+ # key::
+ # Key to be added.
+ # value::
+ # Value to be associated with key.
+ def merge!(hash, key, value)
+ if hash.has_key?(key)
+ if hash[key].instance_of?(Array)
+ hash[key] << value
+ else
+ hash[key] = [hash[key], value]
+ end
+ elsif value.instance_of?(Array)
+ hash[key] = [value]
+ else
+ hash[key] = value
+ end
+ hash
+ end
+
+ # Converts the attributes array of an XML element into a hash.
+ # Returns an empty Hash if node has no attributes.
+ #
+ # element::
+ # XML element to extract attributes from.
+ def get_attributes(element)
+ attributes = {}
+ element.attributes.each { |n,v| attributes[n] = v }
+ attributes
+ end
+
+ # Determines if a document element has text content
+ #
+ # element::
+ # XML element to be checked.
+ def empty_content?(element)
+ element.texts.join.blank?
+ end
+ end
+end
diff --git a/activesupport/test/core_ext/class_test.rb b/activesupport/test/core_ext/class_test.rb
index 9c6071d478..48d8a78acf 100644
--- a/activesupport/test/core_ext/class_test.rb
+++ b/activesupport/test/core_ext/class_test.rb
@@ -19,19 +19,19 @@ class ClassTest < Test::Unit::TestCase
def test_removing_class_in_root_namespace
assert A.is_a?(Class)
Class.remove_class(A)
- assert_raises(NameError) { A.is_a?(Class) }
+ assert_raise(NameError) { A.is_a?(Class) }
end
def test_removing_class_in_one_level_namespace
assert X::B.is_a?(Class)
Class.remove_class(X::B)
- assert_raises(NameError) { X::B.is_a?(Class) }
+ assert_raise(NameError) { X::B.is_a?(Class) }
end
def test_removing_class_in_two_level_namespace
assert Y::Z::C.is_a?(Class)
Class.remove_class(Y::Z::C)
- assert_raises(NameError) { Y::Z::C.is_a?(Class) }
+ assert_raise(NameError) { Y::Z::C.is_a?(Class) }
end
def test_retrieving_subclasses
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index b63ab30965..0edac72fe7 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -174,6 +174,13 @@ class HashExtTest < Test::Unit::TestCase
assert_equal 2, hash['b']
end
+ def test_indifferent_reverse_merging
+ hash = HashWithIndifferentAccess.new('some' => 'value', 'other' => 'value')
+ hash.reverse_merge!(:some => 'noclobber', :another => 'clobber')
+ assert_equal 'value', hash[:some]
+ assert_equal 'clobber', hash[:another]
+ end
+
def test_indifferent_deleting
get_hash = proc{ { :a => 'foo' }.with_indifferent_access }
hash = get_hash.call
@@ -234,7 +241,7 @@ class HashExtTest < Test::Unit::TestCase
{ :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny)
end
- assert_raises(ArgumentError, "Unknown key(s): failore") do
+ assert_raise(ArgumentError, "Unknown key(s): failore") do
{ :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ])
{ :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny)
end
@@ -490,6 +497,15 @@ class HashToXmlTest < Test::Unit::TestCase
assert xml.include?(%(<addresses type="array"><address><streets type="array"><street><name>))
end
+ def test_timezoned_attributes
+ xml = {
+ :created_at => Time.utc(1999,2,2),
+ :local_created_at => Time.utc(1999,2,2).in_time_zone('Eastern Time (US & Canada)')
+ }.to_xml(@xml_options)
+ assert_match %r{<created-at type=\"datetime\">1999-02-02T00:00:00Z</created-at>}, xml
+ assert_match %r{<local-created-at type=\"datetime\">1999-02-01T19:00:00-05:00</local-created-at>}, xml
+ end
+
def test_single_record_from_xml
topic_xml = <<-EOT
<topic>
@@ -884,7 +900,13 @@ class QueryTest < Test::Unit::TestCase
end
def test_expansion_count_is_limited
- assert_raises RuntimeError do
+ expected = {
+ 'ActiveSupport::XmlMini_REXML' => 'RuntimeError',
+ 'ActiveSupport::XmlMini_Nokogiri' => 'Nokogiri::XML::SyntaxError',
+ 'ActiveSupport::XmlMini_LibXML' => 'LibXML::XML::Error',
+ }[ActiveSupport::XmlMini.backend.name].constantize
+
+ assert_raise expected do
attack_xml = <<-EOT
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE member [
diff --git a/activesupport/test/core_ext/load_error_test.rb b/activesupport/test/core_ext/load_error_test.rb
index 5bb5b4d1b8..cfa8f978af 100644
--- a/activesupport/test/core_ext/load_error_test.rb
+++ b/activesupport/test/core_ext/load_error_test.rb
@@ -2,10 +2,10 @@ require 'abstract_unit'
class TestMissingSourceFile < Test::Unit::TestCase
def test_with_require
- assert_raises(MissingSourceFile) { require 'no_this_file_don\'t_exist' }
+ assert_raise(MissingSourceFile) { require 'no_this_file_don\'t_exist' }
end
def test_with_load
- assert_raises(MissingSourceFile) { load 'nor_does_this_one' }
+ assert_raise(MissingSourceFile) { load 'nor_does_this_one' }
end
def test_path
begin load 'nor/this/one.rb'
diff --git a/activesupport/test/core_ext/module/synchronization_test.rb b/activesupport/test/core_ext/module/synchronization_test.rb
index b1d4bc5e06..c28bc9b073 100644
--- a/activesupport/test/core_ext/module/synchronization_test.rb
+++ b/activesupport/test/core_ext/module/synchronization_test.rb
@@ -28,14 +28,14 @@ class SynchronizationTest < Test::Unit::TestCase
end
def test_synchronize_with_no_mutex_raises_an_argument_error
- assert_raises(ArgumentError) do
+ assert_raise(ArgumentError) do
@target.synchronize :to_s
end
end
def test_double_synchronize_raises_an_argument_error
@target.synchronize :to_s, :with => :mutex
- assert_raises(ArgumentError) do
+ assert_raise(ArgumentError) do
@target.synchronize :to_s, :with => :mutex
end
end
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index a5d98507ba..0d3d10f333 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -92,8 +92,8 @@ class ModuleTest < Test::Unit::TestCase
end
def test_missing_delegation_target
- assert_raises(ArgumentError) { eval($nowhere) }
- assert_raises(ArgumentError) { eval($noplace) }
+ assert_raise(ArgumentError) { eval($nowhere) }
+ assert_raise(ArgumentError) { eval($noplace) }
end
def test_delegation_prefix
@@ -141,7 +141,7 @@ class ModuleTest < Test::Unit::TestCase
def test_delegation_without_allow_nil_and_nil_value
david = Someone.new("David")
- assert_raises(NoMethodError) { david.street }
+ assert_raise(NoMethodError) { david.street }
end
def test_parent
@@ -314,7 +314,7 @@ class MethodAliasingTest < Test::Unit::TestCase
alias_method_chain :duck, :orange
end
- assert_raises NoMethodError do
+ assert_raise NoMethodError do
@instance.duck
end
@@ -330,7 +330,7 @@ class MethodAliasingTest < Test::Unit::TestCase
alias_method_chain :duck, :orange
end
- assert_raises NoMethodError do
+ assert_raise NoMethodError do
@instance.duck
end
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 0bdbd14f33..b6515e05a0 100644
--- a/activesupport/test/core_ext/object_and_class_ext_test.rb
+++ b/activesupport/test/core_ext/object_and_class_ext_test.rb
@@ -109,7 +109,7 @@ end
class ObjectTests < Test::Unit::TestCase
def test_suppress_re_raises
- assert_raises(LoadError) { suppress(ArgumentError) {raise LoadError} }
+ assert_raise(LoadError) { suppress(ArgumentError) {raise LoadError} }
end
def test_suppress_supresses
suppress(ArgumentError) { raise ArgumentError }
@@ -256,7 +256,7 @@ class ObjectTryTest < Test::Unit::TestCase
def test_nonexisting_method
method = :undefined_method
assert !@string.respond_to?(method)
- assert_raises(NoMethodError) { @string.try(method) }
+ assert_raise(NoMethodError) { @string.try(method) }
end
def test_valid_method
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index e232bf8384..6c9b7e7236 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -77,6 +77,24 @@ class StringInflectionsTest < Test::Unit::TestCase
end
end
+ def test_string_parameterized_normal
+ StringToParameterized.each do |normal, slugged|
+ assert_equal(normal.parameterize, slugged)
+ end
+ end
+
+ def test_string_parameterized_no_separator
+ StringToParameterizeWithNoSeparator.each do |normal, slugged|
+ assert_equal(normal.parameterize(''), slugged)
+ end
+ end
+
+ def test_string_parameterized_underscore
+ StringToParameterizeWithUnderscore.each do |normal, slugged|
+ assert_equal(normal.parameterize('_'), slugged)
+ end
+ end
+
def test_humanize
UnderscoreToHuman.each do |underscore, human|
assert_equal(human, underscore.humanize)
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index 1de6a2ac2e..accfe51ed6 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -751,7 +751,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
def test_use_zone_with_exception_raised
Time.zone = 'Alaska'
- assert_raises RuntimeError do
+ assert_raise RuntimeError do
Time.use_zone('Hawaii') { raise RuntimeError }
end
assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index fe04b91f2b..a21f09403f 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -43,7 +43,7 @@ class DependenciesTest < Test::Unit::TestCase
end
def test_missing_dependency_raises_missing_source_file
- assert_raises(MissingSourceFile) { require_dependency("missing_service") }
+ assert_raise(MissingSourceFile) { require_dependency("missing_service") }
end
def test_missing_association_raises_nothing
@@ -136,10 +136,10 @@ class DependenciesTest < Test::Unit::TestCase
def test_non_existing_const_raises_name_error
with_loading 'autoloading_fixtures' do
- assert_raises(NameError) { DoesNotExist }
- assert_raises(NameError) { NoModule::DoesNotExist }
- assert_raises(NameError) { A::DoesNotExist }
- assert_raises(NameError) { A::B::DoesNotExist }
+ assert_raise(NameError) { DoesNotExist }
+ assert_raise(NameError) { NoModule::DoesNotExist }
+ assert_raise(NameError) { A::DoesNotExist }
+ assert_raise(NameError) { A::B::DoesNotExist }
end
end
@@ -206,8 +206,8 @@ class DependenciesTest < Test::Unit::TestCase
def failing_test_access_thru_and_upwards_fails
with_loading 'autoloading_fixtures' do
assert ! defined?(ModuleFolder)
- assert_raises(NameError) { ModuleFolder::Object }
- assert_raises(NameError) { ModuleFolder::NestedClass::Object }
+ assert_raise(NameError) { ModuleFolder::Object }
+ assert_raise(NameError) { ModuleFolder::NestedClass::Object }
Object.__send__ :remove_const, :ModuleFolder
end
end
@@ -382,7 +382,7 @@ class DependenciesTest < Test::Unit::TestCase
with_loading 'autoloading_fixtures' do
require_dependency '././counting_loader'
assert_equal 1, $counting_loaded_times
- assert_raises(ArgumentError) { ActiveSupport::Dependencies.load_missing_constant Object, :CountingLoader }
+ assert_raise(ArgumentError) { ActiveSupport::Dependencies.load_missing_constant Object, :CountingLoader }
assert_equal 1, $counting_loaded_times
end
end
@@ -421,7 +421,7 @@ class DependenciesTest < Test::Unit::TestCase
def test_nested_load_error_isnt_rescued
with_loading 'dependencies' do
- assert_raises(MissingSourceFile) do
+ assert_raise(MissingSourceFile) do
RequiresNonexistent1
end
end
@@ -494,7 +494,7 @@ class DependenciesTest < Test::Unit::TestCase
def test_unloadable_should_fail_with_anonymous_modules
with_loading 'autoloading_fixtures' do
m = Module.new
- assert_raises(ArgumentError) { m.unloadable }
+ assert_raise(ArgumentError) { m.unloadable }
end
end
@@ -584,7 +584,7 @@ class DependenciesTest < Test::Unit::TestCase
end
def test_new_constants_in_with_illegal_module_name_raises_correct_error
- assert_raises(NameError) do
+ assert_raise(NameError) do
ActiveSupport::Dependencies.new_constants_in("Illegal-Name") {}
end
end
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index d8c93dc9ae..6b9fbd3156 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -116,6 +116,12 @@ class InflectorTest < Test::Unit::TestCase
end
end
+ def test_parameterize_with_multi_character_separator
+ StringToParameterized.each do |some_string, parameterized_string|
+ assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, '__sep__'))
+ end
+ end
+
def test_classify
ClassNameToTableName.each do |class_name, table_name|
assert_equal(class_name, ActiveSupport::Inflector.classify(table_name))
@@ -161,13 +167,13 @@ class InflectorTest < Test::Unit::TestCase
assert_nothing_raised { assert_equal Ace::Base::Case, ActiveSupport::Inflector.constantize("::Ace::Base::Case") }
assert_nothing_raised { assert_equal InflectorTest, ActiveSupport::Inflector.constantize("InflectorTest") }
assert_nothing_raised { assert_equal InflectorTest, ActiveSupport::Inflector.constantize("::InflectorTest") }
- assert_raises(NameError) { ActiveSupport::Inflector.constantize("UnknownClass") }
- assert_raises(NameError) { ActiveSupport::Inflector.constantize("An invalid string") }
- assert_raises(NameError) { ActiveSupport::Inflector.constantize("InvalidClass\n") }
+ assert_raise(NameError) { ActiveSupport::Inflector.constantize("UnknownClass") }
+ assert_raise(NameError) { ActiveSupport::Inflector.constantize("An invalid string") }
+ assert_raise(NameError) { ActiveSupport::Inflector.constantize("InvalidClass\n") }
end
def test_constantize_does_lexical_lookup
- assert_raises(NameError) { ActiveSupport::Inflector.constantize("Ace::Base::InflectorTest") }
+ assert_raise(NameError) { ActiveSupport::Inflector.constantize("Ace::Base::InflectorTest") }
end
def test_ordinal
diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb
index b7ac467c37..584cbff3e7 100644
--- a/activesupport/test/inflector_test_cases.rb
+++ b/activesupport/test/inflector_test_cases.rb
@@ -154,6 +154,22 @@ module InflectorTestCases
"Squeeze separators" => "squeeze-separators"
}
+ StringToParameterizeWithNoSeparator = {
+ "Donald E. Knuth" => "donaldeknuth",
+ "Random text with *(bad)* characters" => "randomtextwithbadcharacters",
+ "Trailing bad characters!@#" => "trailingbadcharacters",
+ "!@#Leading bad characters" => "leadingbadcharacters",
+ "Squeeze separators" => "squeezeseparators"
+ }
+
+ StringToParameterizeWithUnderscore = {
+ "Donald E. Knuth" => "donald_e_knuth",
+ "Random text with *(bad)* characters" => "random_text_with_bad_characters",
+ "Trailing bad characters!@#" => "trailing_bad_characters",
+ "!@#Leading bad characters" => "leading_bad_characters",
+ "Squeeze separators" => "squeeze_separators"
+ }
+
# Ruby 1.9 doesn't do Unicode normalization yet.
if RUBY_VERSION >= '1.9'
StringToParameterizedAndNormalized = {
diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb
index ebd46a851e..8fe40557d6 100644
--- a/activesupport/test/json/decoding_test.rb
+++ b/activesupport/test/json/decoding_test.rb
@@ -28,7 +28,11 @@ class TestJSONDecoding < Test::Unit::TestCase
%(null) => nil,
%(true) => true,
%(false) => false,
- %q("http:\/\/test.host\/posts\/1") => "http://test.host/posts/1"
+ %q("http:\/\/test.host\/posts\/1") => "http://test.host/posts/1",
+ %q("\u003cunicode\u0020escape\u003e") => "<unicode escape>",
+ %q("\\\\u0020skip double backslashes") => "\\u0020skip double backslashes",
+ %q({a: "\u003cbr /\u003e"}) => {'a' => "<br />"},
+ %q({b:["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => {'b' => ["<i>","<b>","<u>"]}
}
TESTS.each do |json, expected|
@@ -40,6 +44,6 @@ class TestJSONDecoding < Test::Unit::TestCase
end
def test_failed_json_decoding
- assert_raises(ActiveSupport::JSON::ParseError) { ActiveSupport::JSON.decode(%({: 1})) }
+ assert_raise(ActiveSupport::JSON::ParseError) { ActiveSupport::JSON.decode(%({: 1})) }
end
end
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index 2c5b4d0378..7d2eedad61 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -75,7 +75,7 @@ class TestJSONEncoding < Test::Unit::TestCase
def test_exception_raised_when_encoding_circular_reference
a = [1]
a << a
- assert_raises(ActiveSupport::JSON::CircularReferenceError) { a.to_json }
+ assert_raise(ActiveSupport::JSON::CircularReferenceError) { a.to_json }
end
def test_hash_key_identifiers_are_always_quoted
diff --git a/activesupport/test/memoizable_test.rb b/activesupport/test/memoizable_test.rb
index 069ae27eb2..39420c5a3a 100644
--- a/activesupport/test/memoizable_test.rb
+++ b/activesupport/test/memoizable_test.rb
@@ -4,10 +4,12 @@ class MemoizableTest < Test::Unit::TestCase
class Person
extend ActiveSupport::Memoizable
- attr_reader :name_calls, :age_calls
+ attr_reader :name_calls, :age_calls, :is_developer_calls
+
def initialize
@name_calls = 0
@age_calls = 0
+ @is_developer_calls = 0
end
def name
@@ -31,6 +33,14 @@ class MemoizableTest < Test::Unit::TestCase
end
memoize :name, :age
+
+ private
+
+ def is_developer?
+ @is_developer_calls += 1
+ "Yes"
+ end
+ memoize :is_developer?
end
class Company
@@ -223,4 +233,15 @@ class MemoizableTest < Test::Unit::TestCase
company.memoize :name
assert_raise(RuntimeError) { company.memoize :name }
end
+
+ def test_private_method_memoization
+ person = Person.new
+
+ assert_raise(NoMethodError) { person.is_developer? }
+ assert_equal "Yes", person.send(:is_developer?)
+ assert_equal 1, person.is_developer_calls
+ assert_equal "Yes", person.send(:is_developer?)
+ assert_equal 1, person.is_developer_calls
+ end
+
end
diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb
index c0b4a4658c..ed3461571a 100644
--- a/activesupport/test/message_encryptor_test.rb
+++ b/activesupport/test/message_encryptor_test.rb
@@ -33,7 +33,7 @@ class MessageEncryptorTest < Test::Unit::TestCase
private
def assert_not_decrypted(value)
- assert_raises(ActiveSupport::MessageEncryptor::InvalidMessage) do
+ assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do
@encryptor.decrypt(value)
end
end
diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb
index 2190308856..57c4ce841e 100644
--- a/activesupport/test/message_verifier_test.rb
+++ b/activesupport/test/message_verifier_test.rb
@@ -18,7 +18,7 @@ class MessageVerifierTest < Test::Unit::TestCase
end
def assert_not_verified(message)
- assert_raises(ActiveSupport::MessageVerifier::InvalidSignature) do
+ assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
@verifier.verify(message)
end
end
diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb
index 067c461837..661b33cc57 100644
--- a/activesupport/test/multibyte_chars_test.rb
+++ b/activesupport/test/multibyte_chars_test.rb
@@ -26,7 +26,7 @@ class MultibyteCharsTest < Test::Unit::TestCase
assert_nothing_raised do
@chars.__method_for_multibyte_testing
end
- assert_raises NoMethodError do
+ assert_raise NoMethodError do
@chars.__unknown_method
end
end
@@ -71,7 +71,7 @@ class MultibyteCharsTest < Test::Unit::TestCase
end
def test_unpack_raises_encoding_error_on_broken_strings
- assert_raises(ActiveSupport::Multibyte::EncodingError) do
+ assert_raise(ActiveSupport::Multibyte::EncodingError) do
@proxy_class.u_unpack(BYTE_STRING)
end
end
@@ -123,7 +123,6 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase
[:rstrip!, :lstrip!, :strip!, :reverse!, :upcase!, :downcase!, :capitalize!].each do |method|
assert_equal @chars.object_id, @chars.send(method).object_id
end
- assert_equal @chars.object_id, @chars.slice!(1).object_id
end
def test_overridden_bang_methods_change_wrapped_string
@@ -133,10 +132,6 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase
proxy.send(method)
assert_not_equal original, proxy.to_s
end
- proxy = chars('Café')
- proxy.slice!(3)
- assert_equal 'é', proxy.to_s
-
proxy = chars('òu')
proxy.capitalize!
assert_equal 'Òu', proxy.to_s
@@ -214,8 +209,8 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase
end
def test_insert_throws_index_error
- assert_raises(IndexError) { @chars.insert(-12, 'わ')}
- assert_raises(IndexError) { @chars.insert(12, 'わ') }
+ assert_raise(IndexError) { @chars.insert(-12, 'わ')}
+ assert_raise(IndexError) { @chars.insert(12, 'わ') }
end
def test_should_know_if_one_includes_the_other
@@ -227,7 +222,7 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase
end
def test_include_raises_type_error_when_nil_is_passed
- assert_raises(TypeError) do
+ assert_raise(TypeError) do
@chars.include?(nil)
end
end
@@ -262,22 +257,22 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase
def test_indexed_insert_should_raise_on_index_overflow
before = @chars.to_s
- assert_raises(IndexError) { @chars[10] = 'a' }
- assert_raises(IndexError) { @chars[10, 4] = 'a' }
- assert_raises(IndexError) { @chars[/ii/] = 'a' }
- assert_raises(IndexError) { @chars[/()/, 10] = 'a' }
+ assert_raise(IndexError) { @chars[10] = 'a' }
+ assert_raise(IndexError) { @chars[10, 4] = 'a' }
+ assert_raise(IndexError) { @chars[/ii/] = 'a' }
+ assert_raise(IndexError) { @chars[/()/, 10] = 'a' }
assert_equal before, @chars
end
def test_indexed_insert_should_raise_on_range_overflow
before = @chars.to_s
- assert_raises(RangeError) { @chars[10..12] = 'a' }
+ assert_raise(RangeError) { @chars[10..12] = 'a' }
assert_equal before, @chars
end
def test_rjust_should_raise_argument_errors_on_bad_arguments
- assert_raises(ArgumentError) { @chars.rjust(10, '') }
- assert_raises(ArgumentError) { @chars.rjust }
+ assert_raise(ArgumentError) { @chars.rjust(10, '') }
+ assert_raise(ArgumentError) { @chars.rjust }
end
def test_rjust_should_count_characters_instead_of_bytes
@@ -294,8 +289,8 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase
end
def test_ljust_should_raise_argument_errors_on_bad_arguments
- assert_raises(ArgumentError) { @chars.ljust(10, '') }
- assert_raises(ArgumentError) { @chars.ljust }
+ assert_raise(ArgumentError) { @chars.ljust(10, '') }
+ assert_raise(ArgumentError) { @chars.ljust }
end
def test_ljust_should_count_characters_instead_of_bytes
@@ -312,8 +307,8 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase
end
def test_center_should_raise_argument_errors_on_bad_arguments
- assert_raises(ArgumentError) { @chars.center(10, '') }
- assert_raises(ArgumentError) { @chars.center }
+ assert_raise(ArgumentError) { @chars.center(10, '') }
+ assert_raise(ArgumentError) { @chars.center }
end
def test_center_should_count_charactes_instead_of_bytes
@@ -391,6 +386,15 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase
assert_equal nil, @chars.slice(7..6)
end
+ def test_slice_bang_returns_sliced_out_substring
+ assert_equal 'にち', @chars.slice!(1..2)
+ end
+
+ def test_slice_bang_removes_the_slice_from_the_receiver
+ @chars.slice!(1..2)
+ assert_equal 'こわ', @chars
+ end
+
def test_slice_should_throw_exceptions_on_invalid_arguments
assert_raise(TypeError) { @chars.slice(2..3, 1) }
assert_raise(TypeError) { @chars.slice(1, 2..3) }
@@ -436,7 +440,7 @@ class MultibyteCharsExtrasTest < Test::Unit::TestCase
if RUBY_VERSION >= '1.9'
def test_tidy_bytes_is_broken_on_1_9_0
- assert_raises(ArgumentError) do
+ assert_raise(ArgumentError) do
assert_equal_codepoints [0xfffd].pack('U'), chars("\xef\xbf\xbd").tidy_bytes
end
end
diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb
index fb76ca1ab6..7cd8c8a8f4 100644
--- a/activesupport/test/ordered_hash_test.rb
+++ b/activesupport/test/ordered_hash_test.rb
@@ -4,9 +4,11 @@ class OrderedHashTest < Test::Unit::TestCase
def setup
@keys = %w( blue green red pink orange )
@values = %w( 000099 009900 aa0000 cc0066 cc6633 )
+ @hash = Hash.new
@ordered_hash = ActiveSupport::OrderedHash.new
@keys.each_with_index do |key, index|
+ @hash[key] = @values[index]
@ordered_hash[key] = @values[index]
end
end
@@ -17,7 +19,7 @@ class OrderedHashTest < Test::Unit::TestCase
end
def test_access
- assert @keys.zip(@values).all? { |k, v| @ordered_hash[k] == v }
+ assert @hash.all? { |k, v| @ordered_hash[k] == v }
end
def test_assignment
@@ -45,6 +47,10 @@ class OrderedHashTest < Test::Unit::TestCase
assert_nil @ordered_hash.delete(bad_key)
end
+ def test_to_hash
+ assert_same @ordered_hash, @ordered_hash.to_hash
+ end
+
def test_has_key
assert_equal true, @ordered_hash.has_key?('blue')
assert_equal true, @ordered_hash.key?('blue')
@@ -148,4 +154,8 @@ class OrderedHashTest < Test::Unit::TestCase
@ordered_hash.keys.pop
assert_equal original, @ordered_hash.keys
end
-end \ No newline at end of file
+
+ def test_inspect
+ assert @ordered_hash.inspect.include?(@hash.inspect)
+ end
+end
diff --git a/activesupport/test/string_inquirer_test.rb b/activesupport/test/string_inquirer_test.rb
index dda7850e6b..7f11f667df 100644
--- a/activesupport/test/string_inquirer_test.rb
+++ b/activesupport/test/string_inquirer_test.rb
@@ -10,6 +10,6 @@ class StringInquirerTest < Test::Unit::TestCase
end
def test_missing_question_mark
- assert_raises(NoMethodError) { ActiveSupport::StringInquirer.new("production").production }
+ assert_raise(NoMethodError) { ActiveSupport::StringInquirer.new("production").production }
end
end
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index 6dec6a8f34..b01f62460a 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -244,7 +244,7 @@ class TimeZoneTest < Test::Unit::TestCase
assert_nil ActiveSupport::TimeZone["bogus"]
assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone["Central Time (US & Canada)"]
assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone[8]
- assert_raises(ArgumentError) { ActiveSupport::TimeZone[false] }
+ assert_raise(ArgumentError) { ActiveSupport::TimeZone[false] }
end
def test_new
diff --git a/activesupport/test/xml_mini/nokogiri_engine_test.rb b/activesupport/test/xml_mini/nokogiri_engine_test.rb
new file mode 100644
index 0000000000..e5174a0b57
--- /dev/null
+++ b/activesupport/test/xml_mini/nokogiri_engine_test.rb
@@ -0,0 +1,157 @@
+require 'abstract_unit'
+require 'active_support/xml_mini'
+
+begin
+ gem 'nokogiri', '>= 1.1.1'
+rescue Gem::LoadError
+ # Skip nokogiri tests
+else
+
+require 'nokogiri'
+
+class NokogiriEngineTest < Test::Unit::TestCase
+ include ActiveSupport
+
+ def setup
+ @default_backend = XmlMini.backend
+ XmlMini.backend = 'Nokogiri'
+ end
+
+ def teardown
+ XmlMini.backend = @default_backend
+ end
+
+ def test_file_from_xml
+ hash = Hash.from_xml(<<-eoxml)
+ <blog>
+ <logo type="file" name="logo.png" content_type="image/png">
+ </logo>
+ </blog>
+ eoxml
+ assert hash.has_key?('blog')
+ assert hash['blog'].has_key?('logo')
+
+ file = hash['blog']['logo']
+ assert_equal 'logo.png', file.original_filename
+ assert_equal 'image/png', file.content_type
+ end
+
+ def test_exception_thrown_on_expansion_attack
+ assert_raise Nokogiri::XML::SyntaxError do
+ attack_xml = <<-EOT
+ <?xml version="1.0" encoding="UTF-8"?>
+ <!DOCTYPE member [
+ <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
+ <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
+ <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
+ <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
+ <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;">
+ <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;">
+ <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
+ ]>
+ <member>
+ &a;
+ </member>
+ EOT
+ Hash.from_xml(attack_xml)
+ end
+ end
+
+ def test_setting_nokogiri_as_backend
+ XmlMini.backend = 'Nokogiri'
+ assert_equal XmlMini_Nokogiri, XmlMini.backend
+ end
+
+ def test_blank_returns_empty_hash
+ assert_equal({}, XmlMini.parse(nil))
+ assert_equal({}, XmlMini.parse(''))
+ end
+
+ def test_array_type_makes_an_array
+ assert_equal_rexml(<<-eoxml)
+ <blog>
+ <posts type="array">
+ <post>a post</post>
+ <post>another post</post>
+ </posts>
+ </blog>
+ eoxml
+ end
+
+ def test_one_node_document_as_hash
+ assert_equal_rexml(<<-eoxml)
+ <products/>
+ eoxml
+ end
+
+ def test_one_node_with_attributes_document_as_hash
+ assert_equal_rexml(<<-eoxml)
+ <products foo="bar"/>
+ eoxml
+ end
+
+ def test_products_node_with_book_node_as_hash
+ assert_equal_rexml(<<-eoxml)
+ <products>
+ <book name="awesome" id="12345" />
+ </products>
+ eoxml
+ end
+
+ def test_products_node_with_two_book_nodes_as_hash
+ assert_equal_rexml(<<-eoxml)
+ <products>
+ <book name="awesome" id="12345" />
+ <book name="america" id="67890" />
+ </products>
+ eoxml
+ end
+
+ def test_single_node_with_content_as_hash
+ assert_equal_rexml(<<-eoxml)
+ <products>
+ hello world
+ </products>
+ eoxml
+ end
+
+ def test_children_with_children
+ assert_equal_rexml(<<-eoxml)
+ <root>
+ <products>
+ <book name="america" id="67890" />
+ </products>
+ </root>
+ eoxml
+ end
+
+ def test_children_with_text
+ assert_equal_rexml(<<-eoxml)
+ <root>
+ <products>
+ hello everyone
+ </products>
+ </root>
+ eoxml
+ end
+
+ def test_children_with_non_adjacent_text
+ assert_equal_rexml(<<-eoxml)
+ <root>
+ good
+ <products>
+ hello everyone
+ </products>
+ morning
+ </root>
+ eoxml
+ end
+
+ private
+ def assert_equal_rexml(xml)
+ hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
+ assert_equal(hash, XmlMini.parse(xml))
+ end
+end
+
+end
diff --git a/activesupport/test/xml_mini/rexml_engine_test.rb b/activesupport/test/xml_mini/rexml_engine_test.rb
new file mode 100644
index 0000000000..a412d8ca05
--- /dev/null
+++ b/activesupport/test/xml_mini/rexml_engine_test.rb
@@ -0,0 +1,15 @@
+require 'abstract_unit'
+require 'active_support/xml_mini'
+
+class REXMLEngineTest < Test::Unit::TestCase
+ include ActiveSupport
+
+ def test_default_is_rexml
+ assert_equal XmlMini_REXML, XmlMini.backend
+ end
+
+ def test_set_rexml_as_backend
+ XmlMini.backend = 'REXML'
+ assert_equal XmlMini_REXML, XmlMini.backend
+ end
+end
diff --git a/railties/CHANGELOG b/railties/CHANGELOG
index 38c6f808e6..98e3a861e8 100644
--- a/railties/CHANGELOG
+++ b/railties/CHANGELOG
@@ -1,4 +1,10 @@
-*2.3.0 [RC1] (February 1st, 2009)*
+*2.3.2 [Final] (March 15, 2009)*
+
+* Remove outdated script/plugin options [Pratik Naik]
+
+* Allow metal to live in plugins #2045 [Matthew Rudy]
+
+* Added metal [Josh Peek]
* Remove script/performance/request in favour of the performance integration tests. [Pratik Naik]
diff --git a/railties/Rakefile b/railties/Rakefile
index 05f767dc3b..a9adbda0b5 100644
--- a/railties/Rakefile
+++ b/railties/Rakefile
@@ -28,7 +28,8 @@ task :default => :test
## we can see the failures
task :test do
Dir['test/**/*_test.rb'].all? do |file|
- system("ruby -Itest #{file}")
+ ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
+ system(ruby, '-Itest', file)
end or raise "Failures"
end
@@ -246,6 +247,7 @@ end
desc 'Generate guides (for authors), use ONLY=foo to process just "foo.textile"'
task :guides do
+ ENV["WARN_BROKEN_LINKS"] = "1" # authors can't disable this
ruby "guides/rails_guides.rb"
end
@@ -310,11 +312,11 @@ spec = Gem::Specification.new do |s|
EOF
s.add_dependency('rake', '>= 0.8.3')
- s.add_dependency('activesupport', '= 2.3.0' + PKG_BUILD)
- s.add_dependency('activerecord', '= 2.3.0' + PKG_BUILD)
- s.add_dependency('actionpack', '= 2.3.0' + PKG_BUILD)
- s.add_dependency('actionmailer', '= 2.3.0' + PKG_BUILD)
- s.add_dependency('activeresource', '= 2.3.0' + PKG_BUILD)
+ s.add_dependency('activesupport', '= 2.3.2' + PKG_BUILD)
+ s.add_dependency('activerecord', '= 2.3.2' + PKG_BUILD)
+ s.add_dependency('actionpack', '= 2.3.2' + PKG_BUILD)
+ s.add_dependency('actionmailer', '= 2.3.2' + PKG_BUILD)
+ s.add_dependency('activeresource', '= 2.3.2' + PKG_BUILD)
s.rdoc_options << '--exclude' << '.'
s.has_rdoc = false
@@ -343,9 +345,12 @@ task :pgem => [:gem] do
`ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
end
-desc "Publish the API documentation"
-task :pdoc => :rdoc do
- # railties API isn't separately published
+desc "Publish the guides"
+task :pguides => :guides do
+ mkdir_p 'pkg'
+ `tar -czf pkg/guides.gz guides/output`
+ Rake::SshFilePublisher.new("web.rubyonrails.org", "/u/sites/guides.rubyonrails.org/public", "pkg", "guides.gz").upload
+ `ssh web.rubyonrails.org 'cd /u/sites/guides.rubyonrails.org/public/ && tar -xvzf guides.gz && mv guides/output/* . && rm -rf guides*'`
end
desc "Publish the release files to RubyForge."
diff --git a/railties/builtin/rails_info/rails/info.rb b/railties/builtin/rails_info/rails/info.rb
index 7b6f09ac69..a20d9bfe62 100644
--- a/railties/builtin/rails_info/rails/info.rb
+++ b/railties/builtin/rails_info/rails/info.rb
@@ -85,6 +85,10 @@ module Rails
Gem::RubyGemsVersion
end
+ property 'Rack version' do
+ ::Rack.release
+ end
+
# The Rails version.
property 'Rails version' do
Rails::VERSION::STRING
diff --git a/railties/configs/initializers/backtrace_silencers.rb b/railties/configs/initializers/backtrace_silencers.rb
index c2169ed01c..839d4cde19 100644
--- a/railties/configs/initializers/backtrace_silencers.rb
+++ b/railties/configs/initializers/backtrace_silencers.rb
@@ -3,5 +3,5 @@
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
-# You can also remove all the silencers if you're trying do debug a problem that might steem from framework code.
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
# Rails.backtrace_cleaner.remove_silencers! \ No newline at end of file
diff --git a/railties/environments/boot.rb b/railties/environments/boot.rb
index 0a516880ca..0ad0f787f8 100644
--- a/railties/environments/boot.rb
+++ b/railties/environments/boot.rb
@@ -44,6 +44,7 @@ module Rails
def load_initializer
require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
Rails::Initializer.run(:install_gem_spec_stubs)
+ Rails::GemDependency.add_frozen_gem_path
end
end
diff --git a/railties/guides/files/stylesheets/main.css b/railties/guides/files/stylesheets/main.css
index 5061b130e3..d377628d73 100644
--- a/railties/guides/files/stylesheets/main.css
+++ b/railties/guides/files/stylesheets/main.css
@@ -337,7 +337,7 @@ h6 {
margin-top: 0.25em;
}
-#mainCol dd.warning, #subCol dd.warning {
+#mainCol div.warning, #subCol dd.warning {
background: #f9d9d8 url(../../images/tab_red.gif) no-repeat left top;
border: none;
padding: 1.25em 1.25em 1.25em 48px;
@@ -426,4 +426,16 @@ code {
.clearfix {display: inline-block;}
* html .clearfix {height: 1%;}
.clearfix {display: block;}
-.clear { clear:both; } \ No newline at end of file
+.clear { clear:both; }
+
+/* Same bottom margin for special boxes than for regular paragraphs, this way
+intermediate whitespace looks uniform. */
+div.code_container, div.important, div.caution, div.warning, div.note, div.info {
+ margin-bottom: 1.5em;
+}
+
+/* Remove bottom margin of paragraphs in special boxes, otherwise they get a
+spurious blank area below with the box background. */
+div.important p, div.caution p, div.warning p, div.note p, div.info p {
+ margin-bottom: 0px;
+}
diff --git a/railties/guides/images/error_messages.png b/railties/guides/images/error_messages.png
index 32de1cac21..428892194a 100644
--- a/railties/guides/images/error_messages.png
+++ b/railties/guides/images/error_messages.png
Binary files differ
diff --git a/railties/guides/images/fxn.jpg b/railties/guides/images/fxn.jpg
new file mode 100644
index 0000000000..81999341f1
--- /dev/null
+++ b/railties/guides/images/fxn.jpg
Binary files differ
diff --git a/railties/guides/rails_guides.rb b/railties/guides/rails_guides.rb
index 6da7de890e..e0532812e4 100644
--- a/railties/guides/rails_guides.rb
+++ b/railties/guides/rails_guides.rb
@@ -1,17 +1,28 @@
pwd = File.dirname(__FILE__)
$: << pwd
-$: << File.join(pwd, "../../activesupport/lib")
-$: << File.join(pwd, "../../actionpack/lib")
-require "action_controller"
-require "action_view"
+begin
+ as_lib = File.join(pwd, "../../activesupport/lib")
+ ap_lib = File.join(pwd, "../../actionpack/lib")
+
+ $: << as_lib if File.directory?(as_lib)
+ $: << ap_lib if File.directory?(ap_lib)
+
+ require "action_controller"
+ require "action_view"
+rescue LoadError
+ require 'rubygems'
+ gem "actionpack", '>= 2.3'
+
+ require "action_controller"
+ require "action_view"
+end
-# Require rubygems after loading Action View
-require 'rubygems'
begin
- gem 'RedCloth', '>= 4.1.1'# Need exactly 4.1.1
+ require 'rubygems'
+ gem 'RedCloth', '>= 4.1.1'
rescue Gem::LoadError
- $stderr.puts %(Missing the RedCloth 4.1.1 gem.\nPlease `gem install -v=4.1.1 RedCloth` to generate the guides.)
+ $stderr.puts %(Generating Guides requires RedCloth 4.1.1+)
exit 1
end
@@ -22,6 +33,7 @@ module RailsGuides
autoload :Indexer, "rails_guides/indexer"
autoload :Helpers, "rails_guides/helpers"
autoload :TextileExtensions, "rails_guides/textile_extensions"
+ autoload :Levenshtein, "rails_guides/levenshtein"
end
RedCloth.send(:include, RailsGuides::TextileExtensions)
diff --git a/railties/guides/rails_guides/generator.rb b/railties/guides/rails_guides/generator.rb
index dd147c4d5f..f93282db2e 100644
--- a/railties/guides/rails_guides/generator.rb
+++ b/railties/guides/rails_guides/generator.rb
@@ -57,7 +57,7 @@ module RailsGuides
result = view.render(:layout => 'layout', :text => textile(body))
f.write result
- warn_about_broken_links(result)
+ warn_about_broken_links(result) if ENV.key?("WARN_BROKEN_LINKS")
end
end
end
@@ -137,16 +137,34 @@ module RailsGuides
end
def warn_about_broken_links(html)
+ anchors = extract_anchors(html)
+ check_fragment_identifiers(html, anchors)
+ end
+
+ def extract_anchors(html)
# Textile generates headers with IDs computed from titles.
- anchors = Set.new(html.scan(/<h\d\s+id="([^"]+)/).flatten)
+ anchors = Set.new
+ html.scan(/<h\d\s+id="([^"]+)/).flatten.each do |anchor|
+ if anchors.member?(anchor)
+ puts "*** DUPLICATE HEADER ID: #{anchor}, please consider rewording" if ENV.key?("WARN_DUPLICATE_HEADERS")
+ else
+ anchors << anchor
+ end
+ end
+
# Also, footnotes are rendered as paragraphs this way.
anchors += Set.new(html.scan(/<p\s+class="footnote"\s+id="([^"]+)/).flatten)
-
- # Check fragment identifiers.
+ return anchors
+ end
+
+ def check_fragment_identifiers(html, anchors)
html.scan(/<a\s+href="#([^"]+)/).flatten.each do |fragment_identifier|
next if fragment_identifier == 'mainCol' # in layout, jumps to some DIV
unless anchors.member?(fragment_identifier)
- puts "BROKEN LINK: ##{fragment_identifier}"
+ guess = anchors.min { |a, b|
+ Levenshtein.distance(fragment_identifier, a) <=> Levenshtein.distance(fragment_identifier, b)
+ }
+ puts "*** BROKEN LINK: ##{fragment_identifier}, perhaps you meant ##{guess}."
end
end
end
diff --git a/railties/guides/rails_guides/indexer.rb b/railties/guides/rails_guides/indexer.rb
index 7cb254d0b0..5b5ad3fee1 100644
--- a/railties/guides/rails_guides/indexer.rb
+++ b/railties/guides/rails_guides/indexer.rb
@@ -29,7 +29,7 @@ module RailsGuides
return level_hash
elsif level == current_level
index = counters.join(".")
- bookmark = '#' + title.gsub(/[^a-z0-9\-_]+/i, '').underscore.dasherize
+ bookmark = '#' + title.strip.downcase.gsub(/\s+|_/, '-').delete('^a-z0-9-')
raise "Parsing Fail" unless @result.sub!(matched, "h#{level}(#{bookmark}). #{index}#{title}")
diff --git a/railties/guides/rails_guides/levenshtein.rb b/railties/guides/rails_guides/levenshtein.rb
new file mode 100644
index 0000000000..4010b61e26
--- /dev/null
+++ b/railties/guides/rails_guides/levenshtein.rb
@@ -0,0 +1,29 @@
+module Levenshtein
+ # Based on the pseudocode in http://en.wikipedia.org/wiki/Levenshtein_distance.
+ def self.distance(s1, s2)
+ s = s1.unpack('U*')
+ t = s2.unpack('U*')
+ m = s.length
+ n = t.length
+
+ # matrix initialization
+ d = []
+ 0.upto(m) { |i| d << [i] }
+ 0.upto(n) { |j| d[0][j] = j }
+
+ # distance computation
+ 1.upto(m) do |i|
+ 1.upto(n) do |j|
+ cost = s[i] == t[j] ? 0 : 1
+ d[i][j] = [
+ d[i-1][j] + 1, # deletion
+ d[i][j-1] + 1, # insertion
+ d[i-1][j-1] + cost, # substitution
+ ].min
+ end
+ end
+
+ # all done
+ return d[m][n]
+ end
+end
diff --git a/railties/guides/source/2_3_release_notes.textile b/railties/guides/source/2_3_release_notes.textile
index c58cbc0b81..6a97fd2cd1 100644
--- a/railties/guides/source/2_3_release_notes.textile
+++ b/railties/guides/source/2_3_release_notes.textile
@@ -1,7 +1,5 @@
h2. Ruby on Rails 2.3 Release Notes
-NOTE: These release notes refer to RC2 of Rails 2.3. This is a release candidate, and not the final version of Rails 2.3. It's intended to be a stable testing release, and we urge you to test your own applications and report any issues to the "Rails Lighthouse":http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/overview.
-
Rails 2.3 delivers a variety of new and improved features, including pervasive Rack integration, refreshed support for Rails Engines, nested transactions for Active Record, dynamic and default scopes, unified rendering, more efficient routing, application templates, and quiet backtraces. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the "list of commits":http://github.com/rails/rails/commits/master in the main Rails repository on GitHub or review the +CHANGELOG+ files for the individual Rails components.
endprologue.
@@ -22,24 +20,25 @@ Rails has now broken with its CGI past, and uses Rack everywhere. This required
Here's a summary of the rack-related changes:
* +script/server+ has been switched to use Rack, which means it supports any Rack compatible server. +script/server+ will also pick up a rackup configuration file if one exists. By default, it will look for a +config.ru+ file, but you can override this with the +-c+ switch.
-* The FCGI handler goes through Rack
-* +ActionController::Dispatcher+ maintains its own default middleware stack. Middlewares can be injected in, reordered, and removed. The stack is compiled into a chain on boot. You can configure the middleware stack in +environment.rb+
+* The FCGI handler goes through Rack.
+* +ActionController::Dispatcher+ maintains its own default middleware stack. Middlewares can be injected in, reordered, and removed. The stack is compiled into a chain on boot. You can configure the middleware stack in +environment.rb+.
* The +rake middleware+ task has been added to inspect the middleware stack. This is useful for debugging the order of the middleware stack.
* The integration test runner has been modified to execute the entire middleware and application stack. This makes integration tests perfect for testing Rack middleware.
* +ActionController::CGIHandler+ is a backwards compatible CGI wrapper around Rack. The +CGIHandler+ is meant to take an old CGI object and convert its environment information into a Rack compatible form.
-* +CgiRequest+ and +CgiResponse+ have been removed
+* +CgiRequest+ and +CgiResponse+ have been removed.
* Session stores are now lazy loaded. If you never access the session object during a request, it will never attempt to load the session data (parse the cookie, load the data from memcache, or lookup an Active Record object).
-* +CGI::Session::CookieStore+ has been replaced by +ActionController::Session::CookieStore+
-* +CGI::Session::MemCacheStore+ has been replaced by +ActionController::Session::MemCacheStore+
-* +CGI::Session::ActiveRecordStore+ has been replaced by +ActiveRecord::SessionStore+
-* You can still change your session store with +ActionController::Base.session_store = :active_record_store+
-* Default sessions options are still set with +ActionController::Base.session = { :key => "..." }+
-* The mutex that normally wraps your entire request has been moved into middleware, +ActionController::Lock+
+* You no longer need to use +CGI::Cookie.new+ in your tests for setting a cookie value. Assigning a +String+ value to request.cookies["foo"] now sets the cookie as expected.
+* +CGI::Session::CookieStore+ has been replaced by +ActionController::Session::CookieStore+.
+* +CGI::Session::MemCacheStore+ has been replaced by +ActionController::Session::MemCacheStore+.
+* +CGI::Session::ActiveRecordStore+ has been replaced by +ActiveRecord::SessionStore+.
+* You can still change your session store with +ActionController::Base.session_store = :active_record_store+.
+* Default sessions options are still set with +ActionController::Base.session = { :key => "..." }+. However, the +:session_domain+ option has been renamed to +:domain+.
+* The mutex that normally wraps your entire request has been moved into middleware, +ActionController::Lock+.
* +ActionController::AbstractRequest+ and +ActionController::Request+ have been unified. The new +ActionController::Request+ inherits from +Rack::Request+. This affects access to +response.headers['type']+ in test requests. Use +response.content_type+ instead.
* +ActiveRecord::QueryCache+ middleware is automatically inserted onto the middleware stack if +ActiveRecord+ has been loaded. This middleware sets up and flushes the per-request Active Record query cache.
* The Rails router and controller classes follow the Rack spec. You can call a controller directly with +SomeController.call(env)+. The router stores the routing parameters in +rack.routing_args+.
-* +ActionController::Request+ inherits from +Rack::Request+
-* Instead of +config.action_controller.session = { :session_key => 'foo', ...+ use +config.action_controller.session = { :key => 'foo', ...+
+* +ActionController::Request+ inherits from +Rack::Request+.
+* Instead of +config.action_controller.session = { :session_key => 'foo', ...+ use +config.action_controller.session = { :key => 'foo', ...+.
* Using the +ParamsParser+ middleware preprocesses any XML, JSON, or YAML requests so they can be read normally with any +Rack::Request+ object after it.
h4. Renewed Support for Rails Engines
@@ -50,7 +49,7 @@ h3. Documentation
The "Ruby on Rails guides":http://guides.rubyonrails.org/ project has published several additional guides for Rails 2.3. In addition, a "separate site":http://guides.rails.info/ maintains updated copies of the Guides for Edge Rails. Other documentation efforts include a relaunch of the "Rails wiki":http://newwiki.rubyonrails.org/ and early planning for a Rails Book.
-* More Information: "Rails Documentation Projects":http://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects
+* More Information: "Rails Documentation Projects":http://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects.
h3. Ruby 1.9.1 Support
@@ -140,19 +139,19 @@ end
You can pass most of the +find+ options into +find_in_batches+. However, you cannot specify the order that records will be returned in (they will always be returned in ascending order of primary key, which must be an integer), or use the +:limit+ option. Instead, use the +:batch_size+ option, which defaults to 1000, to set the number of records that will be returned in each batch.
-The new +each+ method provides a wrapper around +find_in_batches+ that returns individual records, with the find itself being done in batches (of 1000 by default):
+The new +find_each+ method provides a wrapper around +find_in_batches+ that returns individual records, with the find itself being done in batches (of 1000 by default):
<ruby>
-Customer.each do |customer|
+Customer.find_each do |customer|
customer.update_account_balance!
end
</ruby>
Note that you should only use this method for batch processing: for small numbers of records (less than 1000), you should just use the regular find methods with your own loop.
-* More Information:
- - "Rails 2.3: Batch Finding":http://afreshcup.com/2009/02/23/rails-23-batch-finding/
- - "What's New in Edge Rails: Batched Find":http://ryandaigle.com/articles/2009/2/23/what-s-new-in-edge-rails-batched-find
+* More Information (at that point the convenience method was called just +each+):
+** "Rails 2.3: Batch Finding":http://afreshcup.com/2009/02/23/rails-23-batch-finding/
+** "What's New in Edge Rails: Batched Find":http://ryandaigle.com/articles/2009/2/23/what-s-new-in-edge-rails-batched-find
h4. Multiple Conditions for Callbacks
@@ -175,18 +174,6 @@ developers = Developer.find(:all, :group => "salary",
* Lead Contributor: "Emilio Tagua":http://github.com/miloops
-h4. Hash Conditions for has_many relationships
-
-You can once again use a hash in conditions for a +has_many+ relationship:
-
-<ruby>
-has_many :orders, :conditions => {:status => 'confirmed'}
-</ruby>
-
-That worked in Rails 2.1, fails in Rails 2.2, and will now work again in Rails 2.3 (if you're dealing with this issue in Rails 2.2, you can use a string rather than a hash to specify conditions).
-
-* Lead Contributor: "Frederick Cheung":http://www.spacevatican.org/
-
h4. Reconnecting MySQL Connections
MySQL supports a reconnect flag in its connections - if set to true, then the client will try reconnecting to the server before giving up in case of a lost connection. You can now set +reconnect = true+ for your MySQL connections in +database.yml+ to get this behavior from a Rails application. The default is +false+, so the behavior of existing applications doesn't change.
@@ -198,15 +185,17 @@ MySQL supports a reconnect flag in its connections - if set to true, then the cl
h4. Other Active Record Changes
-* An extra +AS+ was removed from the generated SQL for has_and_belongs_to_many preloading, making it work better for some databases.
+* An extra +AS+ was removed from the generated SQL for +has_and_belongs_to_many+ preloading, making it work better for some databases.
* +ActiveRecord::Base#new_record?+ now returns +false+ rather than +nil+ when confronted with an existing record.
* A bug in quoting table names in some +has_many :through+ associations was fixed.
* You can now specify a particular timestamp for +updated_at+ timestamps: +cust = Customer.create(:name => "ABC Industries", :updated_at => 1.day.ago)+
* Better error messages on failed +find_by_attribute!+ calls.
* Active Record's +to_xml+ support gets just a little bit more flexible with the addition of a +:camelize+ option.
-* A bug in canceling callbacks from +before_update+ or +before_create_ was fixed.
+* A bug in canceling callbacks from +before_update+ or +before_create+ was fixed.
* Rake tasks for testing databases via JDBC have been added.
-* +validates_length_of+ will use a custom error message with the +:in+ or +:within+ options (if one is supplied)
+* +validates_length_of+ will use a custom error message with the +:in+ or +:within+ options (if one is supplied).
+* Counts on scoped selects now work properly, so you can do things like +Account.scoped(:select => "DISTINCT credit_limit").count+.
+* +ActiveRecord::Base#invalid?+ now works as the opposite of +ActiveRecord::Base#valid?+.
h3. Action Controller
@@ -299,7 +288,7 @@ In some of the first fruits of the Rails-Merb team merger, Rails 2.3 includes so
h4. Improved Caching Performance
-Rails now keeps a per-request local cache of requests, cutting down on unnecessary reads and leading to better site performance. While this work was originally limited to +MemCacheStore+, it is available to any remote store than implements the required methods.
+Rails now keeps a per-request local cache of read from the remote cache stores, cutting down on unnecessary reads and leading to better site performance. While this work was originally limited to +MemCacheStore+, it is available to any remote store than implements the required methods.
* Lead Contributor: "Nahum Wild":http://www.motionstandingstill.com/
@@ -307,6 +296,8 @@ h4. Localized Views
Rails can now provide localized views, depending on the locale that you have set. For example, suppose you have a +Posts+ controller with a +show+ action. By default, this will render +app/views/posts/show.html.erb+. But if you set +I18n.locale = :da+, it will render +app/views/posts/show.da.html.erb+. If the localized template isn't present, the undecorated version will be used. Rails also includes +I18n#available_locales+ and +I18n::SimpleBackend#available_locales+, which return an array of the translations that are available in the current Rails project.
+In addition, you can use the same scheme to localize the rescue files in the +public+ directory: +public/500.da.html+ or +public/404.en.html+ work, for example.
+
h4. Partial Scoping for Translations
A change to the translation API makes things easier and less repetitive to write key translations within partials. If you call +translate(".foo")+ from the +people/index.html.erb+ template, you'll actually be calling +I18n.translate("people.index.foo")+ If you don't prepend the key with a period, then the API doesn't scope, just as before.
@@ -321,6 +312,9 @@ h4. Other Action Controller Changes
* The +:only+ and +:except+ options for +map.resources+ are no longer inherited by nested resources.
* The bundled memcached client has been updated to version 1.6.4.99.
* The +expires_in+, +stale?+, and +fresh_when+ methods now accept a +:public+ option to make them work well with proxy caching.
+* The +:requirements+ option now works properly with additional RESTful member routes.
+* Shallow routes now properly respect namespaces.
+* +polymorphic_url+ does a better job of handling objects with irregular plural names.
h3. Action View
@@ -439,6 +433,34 @@ returns
</optgroup>
</ruby>
+h4. Disabled Option Tags for Form Select Helpers
+
+The form select helpers (such as +select+ and +options_for_select+) now support a +:disabled+ option, which can take a single value or an array of values to be disabled in the resulting tags:
+
+<ruby>
+select(:post, :category, Post::CATEGORIES, :disabled => ‘private‘)
+</ruby>
+
+returns
+
+<ruby>
+<select name=“post[category]“>
+<option>story</option>
+<option>joke</option>
+<option>poem</option>
+<option disabled=“disabled“>private</option>
+</select>
+</ruby>
+
+You can also use an anonymous function to determine at runtime which options from collections will be selected and/or disabled:
+
+<ruby>
+options_from_collection_for_select(@product.sizes, :name, :id, :disabled => lambda{|size| size.out_of_stock?})
+</ruby>
+
+* Lead Contributor: "Tekin Suleyman":http://tekin.co.uk/
+* More Information: "New in rails 2.3 - disabled option tags and lambdas for selecting and disabling options from collections":http://tekin.co.uk/2009/03/new-in-rails-23-disabled-option-tags-and-lambdas-for-selecting-and-disabling-options-from-collections/
+
h4. A Note About Template Loading
Rails 2.3 includes the ability to enable or disable cached templates for any particular environment. Cached templates give you a speed boost because they don't check for a new template file when they're rendered - but they also mean that you can't replace a template "on the fly" without restarting the server.
@@ -472,6 +494,17 @@ h4. Object#tap Backport
+Object#tap+ is an addition to "Ruby 1.9":http://www.ruby-doc.org/core-1.9/classes/Object.html#M000309 and 1.8.7 that is similar to the +returning+ method that Rails has had for a while: it yields to a block, and then returns the object that was yielded. Rails now includes code to make this available under older versions of Ruby as well.
+h4. Swappable Parsers for XMLmini
+
+The support for XML parsing in ActiveSupport has been made more flexible by allowing you to swap in different parsers. By default, it uses the standard REXML implementation, but you can easily specify the faster LibXML or Nokogiri implementations for your own applications, provided you have the appropriate gems installed:
+
+<ruby>
+XmlMini.backend = 'LibXML'
+</ruby>
+
+* Lead Contributor: "Bart ten Brinke":http://www.movesonrails.com/
+* Lead Contributor: "Aaron Patterson":http://tenderlovemaking.com/
+
h4. Fractional seconds for TimeWithZone
The +Time+ and +TimeWithZone+ classes include an +xmlschema+ method to return the time in an XML-friendly string. As of Rails 2.3, +TimeWithZone+ supports the same argument for specifying the number of digits in the fractional second part of the returned string that +Time+ does:
@@ -494,6 +527,10 @@ h4. Other Active Support Changes
* +ActiveSupport::OrderedHash+: now implements +each_key+ and +each_value+.
* +ActiveSupport::MessageEncryptor+ provides a simple way to encrypt information for storage in an untrusted location (like cookies).
* Active Support's +from_xml+ no longer depends on XmlSimple. Instead, Rails now includes its own XmlMini implementation, with just the functionality that it requires. This lets Rails dispense with the bundled copy of XmlSimple that it's been carting around.
+* If you memoize a private method, the result will now be private.
+* +String#parameterize+ accepts an optional separator: +"Quick Brown Fox".parameterize('_') => "quick_brown_fox"+.
+* +number_to_phone+ accepts 7-digit phone numbers now.
+* +ActiveSupport::Json.decode+ now handles +\u0000+ style escape sequences.
h3. Railties
@@ -532,6 +569,12 @@ Quite a bit of work was done to make sure that bits of Rails (and its dependenci
You can also specify (by using the new +preload_frameworks+ option) whether the core libraries should be autoloaded at startup. This defaults to +false+ so that Rails autoloads itself piece-by-piece, but there are some circumstances where you still need to bring in everything at once - Passenger and JRuby both want to see all of Rails loaded together.
+h4. rake gem Task Rewrite
+
+The internals of the various <code>rake gem</code> tasks have been substantially revised, to make the system work better for a variety of cases. The gem system now knows the difference between development and runtime dependencies, has a more robust unpacking system, gives better information when querying for the status of gems, and is less prone to "chicken and egg" dependency issues when you're bringing things up from scratch. There are also fixes for using gem commands under JRuby and for dependencies that try to bring in external copies of gems that are already vendored.
+
+* Lead Contributor: "David Dollar":http://www.workingwithrails.com/person/12240-david-dollar
+
h4. Other Railties Changes
* The instructions for updating a CI server to build Rails have been updated and expanded.
@@ -543,6 +586,8 @@ h4. Other Railties Changes
* Rails Guides have been converted from AsciiDoc to Textile markup.
* Scaffolded views and controllers have been cleaned up a bit.
* +script/server+ now accepts a <tt>--path</tt> argument to mount a Rails application from a specific path.
+* If any configured gems are missing, the gem rake tasks will skip loading much of the environment. This should solve many of the "chicken-and-egg" problems where rake gems:install couldn't run because gems were missing.
+* Gems are now unpacked exactly once. This fixes issues with gems (hoe, for instance) which are packed with read-only permissions on the files.
h3. Deprecated
diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile
index 949a962b27..054ca99985 100644
--- a/railties/guides/source/action_controller_overview.textile
+++ b/railties/guides/source/action_controller_overview.textile
@@ -20,7 +20,7 @@ For most conventional RESTful applications, the controller will receive the requ
A controller can thus be thought of as a middle man between models and views. It makes the model data available to the view so it can display that data to the user, and it saves or updates data from the user to the model.
-NOTE: For more details on the routing process, see "Rails Routing from the Outside In":routing_outside_in.html.
+NOTE: For more details on the routing process, see "Rails Routing from the Outside In":routing.html.
h3. Methods and Actions
@@ -83,7 +83,7 @@ class ClientsController < ActionController::Base
end
</ruby>
-h4. Hash and array parameters
+h4. Hash and Array Parameters
The +params+ hash is not limited to one-dimensional keys and values. It can contain arrays and (nested) hashes. To send an array of values, append an empty pair of square brackets "[]" to the key name:
@@ -123,7 +123,7 @@ map.connect "/clients/:status",
In this case, when a user opens the URL +/clients/active+, +params[:status]+ will be set to "active". When this route is used, +params[:foo]+ will also be set to "bar" just like it was passed in the query string. In the same way +params[:action]+ will contain "index".
-h4. default_url_options
+h4. +default_url_options+
You can set global default parameters that will be used when generating URLs with +default_url_options+. To do this, define a method with that name in your controller:
@@ -180,7 +180,7 @@ ActionController::Base.session = {
NOTE: Changing the secret when using the CookieStore will invalidate all existing sessions.
-h4. Accessing the session
+h4. Accessing the Session
In your controller you can access the session through the +session+ instance method.
@@ -235,7 +235,7 @@ end
To reset the entire session, use +reset_session+.
-h4. The flash
+h4. The Flash
The flash is a special part of the session which is cleared with each request. This means that values stored there will only be available in the next request, which is useful for storing error messages etc. It is accessed in much the same way as the session, like a hash. Let's use the act of logging out as an example. The controller can send a message which will be displayed to the user on the next request:
@@ -288,7 +288,7 @@ class MainController < ApplicationController
end
</ruby>
-h5. flash.now
+h5. +flash.now+
By default, adding values to the flash will make them available to the next request, but sometimes you may want to access those values in the same request. For example, if the +create+ action fails to save a resource and you render the +new+ template directly, that's not going to result in a new request, but you may still want to display a message using the flash. To do this, you can use +flash.now+ in the same way you use the normal +flash+:
@@ -381,7 +381,7 @@ end
Now, the +LoginsController+'s +new+ and +create+ actions will work as before without requiring the user to be logged in. The +:only+ option is used to only skip this filter for these actions, and there is also an +:except+ option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place.
-h4. After filters and around filters
+h4. After Filters and Around Filters
In addition to before filters, you can run filters after an action has run or both before and after. The after filter is similar to the before filter, but because the action has already been run it has access to the response data that's about to be sent to the client. Obviously, after filters can not stop the action from running.
@@ -403,7 +403,7 @@ private
end
</ruby>
-h4. Other ways to use filters
+h4. Other Ways to Use Filters
While the most common way to use filters is by creating private methods and using *_filter to add them, there are two other ways to do the same thing.
@@ -517,7 +517,7 @@ h3. The Request and Response Objects
In every controller there are two accessor methods pointing to the request and the response objects associated with the request cycle that is currently in execution. The +request+ method contains an instance of +AbstractRequest+ and the +response+ method returns a response object representing what is going to be sent back to the client.
-h4. The +request+ object
+h4. The +request+ Object
The request object contains a lot of useful information about the request coming in from the client. To get a full list of the available methods, refer to the "API documentation":http://api.rubyonrails.org/classes/ActionController/AbstractRequest.html. Among the properties that you can access on this object are:
@@ -538,7 +538,7 @@ h5. +path_parameters+, +query_parameters+, and +request_parameters+
Rails collects all of the parameters sent along with the request in the +params+ hash, whether they are sent as part of the query string or the post body. The request object has three accessors that give you access to these parameters depending on where they came from. The +query_parameters+ hash contains parameters that were sent as part of the query string while the +request_parameters+ hash contains parameters sent as part of the post body. The +path_parameters+ hash contains parameters that were recognized by the routing as being part of the path leading to this particular controller and action.
-h4. The response object
+h4. The +response+ Object
The response object is not usually used directly, but is built up during the execution of the action and rendering of the data that is being sent back to the user, but sometimes - like in an after filter - it can be useful to access the response directly. Some of these accessor methods also have setters, allowing you to change their values.
@@ -550,7 +550,7 @@ The response object is not usually used directly, but is built up during the exe
|charset|The character set being used for the response. Default is "utf-8".|
|headers|Headers used for the response.|
-h5. Setting custom headers
+h5. Setting Custom Headers
If you want to set custom headers for a response then +response.headers+ is the place to do it. The headers attribute is a hash which maps header names to their values, and Rails will set some of them automatically. If you want to add or change a header, just assign it to +response.headers+ this way:
@@ -565,7 +565,7 @@ Rails comes with two built-in HTTP authentication mechanisms:
* Basic Authentication
* Digest Authentication
-h4. HTTP basic authentication
+h4. HTTP Basic Authentication
HTTP basic authentication is an authentication scheme that is supported by the majority of browsers and other HTTP clients. As an example, consider an administration section which will only be available by entering a username and a password into the browser's HTTP basic dialog window. Using the built-in authentication is quite easy and only requires you to use one method, +authenticate_or_request_with_http_basic+.
@@ -587,7 +587,7 @@ end
With this in place, you can create namespaced controllers that inherit from +AdminController+. The before filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication.
-h4. HTTP digest authentication
+h4. HTTP Digest Authentication
HTTP digest authentication is superior to the basic authentication as it does not require the client to send an unencrypted password over the network (though HTTP basic authentication is safe over HTTPS). Using digest authentication with Rails is quite easy and only requires using one method, +authenticate_or_request_with_http_digest+.
@@ -640,7 +640,7 @@ end
The +download_pdf+ action in the example above will call a private method which actually generates the PDF document and returns it as a string. This string will then be streamed to the client as a file download and a filename will be suggested to the user. Sometimes when streaming files to the user, you may not want them to download the file. Take images, for example, which can be embedded into HTML pages. To tell the browser a file is not meant to be downloaded, you can set the +:disposition+ option to "inline". The opposite and default value for this option is "attachment".
-h4. Sending files
+h4. Sending Files
If you want to send a file that already exists on disk, use the +send_file+ method.
@@ -662,7 +662,7 @@ WARNING: Be careful when using data coming from the client (params, cookies, etc
TIP: It is not recommended that you stream static files through Rails if you can instead keep them in a public folder on your web server. It is much more efficient to let the user download the file directly using Apache or another web server, keeping the request from unnecessarily going through the whole Rails stack. Although if you do need the request to go through Rails for some reason, you can set the +:x_sendfile+ option to true, and Rails will let the web server handle sending the file to the user, freeing up the Rails process to do other things. Note that your web server needs to support the +X-Sendfile+ header for this to work.
-h4. RESTful downloads
+h4. RESTful Downloads
While +send_data+ works just fine, if you are creating a RESTful application having separate actions for file downloads is usually not necessary. In REST terminology, the PDF file from the example above can be considered just another representation of the client resource. Rails provides an easy and quite sleek way of doing "RESTful downloads". Here's how you can rewrite the example so that the PDF download is a part of the +show+ action, without any streaming:
@@ -712,7 +712,7 @@ Most likely your application is going to contain bugs or otherwise throw an exce
Rails' default exception handling displays a "500 Server Error" message for all exceptions. If the request was made locally, a nice traceback and some added information gets displayed so you can figure out what went wrong and deal with it. If the request was remote Rails will just display a simple "500 Server Error" message to the user, or a "404 Not Found" if there was a routing error or a record could not be found. Sometimes you might want to customize how these errors are caught and how they're displayed to the user. There are several levels of exception handling available in a Rails application:
-h4. The default 500 and 404 templates
+h4. The Default 500 and 404 Templates
By default a production application will render either a 404 or a 500 error message. These messages are contained in static HTML files in the +public+ folder, in +404.html+ and +500.html+ respectively. You can customize these files to add some extra information and layout, but remember that they are static; i.e. you can't use RHTML or layouts in them, just plain HTML.
diff --git a/railties/guides/source/action_mailer_basics.textile b/railties/guides/source/action_mailer_basics.textile
index 71398382be..9476635ae6 100644
--- a/railties/guides/source/action_mailer_basics.textile
+++ b/railties/guides/source/action_mailer_basics.textile
@@ -12,9 +12,9 @@ h3. Sending Emails
This section will provide a step-by-step guide to creating a mailer and its views.
-h4. Walkthrough to generating a mailer
+h4. Walkthrough to Generating a Mailer
-h5. Create the mailer:
+h5. Create the Mailer
<shell>
./script/generate mailer UserMailer
@@ -28,7 +28,7 @@ create test/unit/user_mailer_test.rb
So we got the model, the fixtures, and the tests.
-h5. Edit the model:
+h5. Edit the Model
+app/models/user_mailer.rb+ contains an empty mailer:
@@ -60,7 +60,7 @@ Here is a quick explanation of the options presented in the preceding method. Fo
The keys of the hash passed to +body+ become instance variables in the view. Thus, in our example the mailer view will have a +@user+ and a +@url+ instance variables available.
-h5. Create a mailer view
+h5. Create a Mailer View
Create a file called +welcome_email.text.html.erb+ in +app/views/user_mailer/+. This will be the template used for the email, formatted in HTML:
@@ -83,7 +83,7 @@ Create a file called +welcome_email.text.html.erb+ in +app/views/user_mailer/+.
Had we wanted to send text-only emails, the file would have been called +welcome_email.text.plain.erb+. Rails sets the content type of the email to be the one in the filename.
-h5. Wire it up so that the system sends the email when a user signs up
+h5. Wire It Up So That the System Sends the Email When a User Signs Up
There are three ways to achieve this. One is to send the email from the controller that sends the email, another is to put it in a +before_create+ callback in the user model, and the last one is to use an observer on the user model. Whether you use the second or third methods is up to you, but staying away from the first is recommended. Not because it's wrong, but because it keeps your controller clean, and keeps all logic related to the user model within the user model. This way, whichever way a user is created (from a web form, or from an API call, for example), we are guaranteed that the email will be sent.
@@ -112,7 +112,7 @@ end
Notice how we call +deliver_welcome_email+? In Action Mailer we send emails by calling +deliver_&lt;method_name&gt;+. In UserMailer, we defined a method called +welcome_email+, and so we deliver the email by calling +deliver_welcome_email+. The next section will go through how Action Mailer achieves this.
-h4. Action Mailer and dynamic deliver_&lt;method_name&gt; methods
+h4. Action Mailer and Dynamic +deliver_&lt;method_name&gt;+ methods
So how does Action Mailer understand this +deliver_welcome_email+ call? If you read the documentation (http://api.rubyonrails.org/files/vendor/rails/actionmailer/README.html), you will find this in the "Sending Emails" section:
@@ -135,7 +135,7 @@ end
Hence, if the method name starts with +deliver_+ followed by any combination of lowercase letters or underscore, +method_missing+ calls +new+ on your mailer class (+UserMailer+ in our example above), sending the combination of lower case letters or underscore, along with the parameters. The resulting object is then sent the +deliver!+ method, which well... delivers it.
-h4. Complete list of Action Mailer user-settable attributes
+h4. Complete List of Action Mailer User-Settable Attributes
|bcc| The BCC addresses of the email|
|body| The body of the email. This is either a hash (in which case it specifies the variables to pass to the template when it is rendered), or a string, in which case it specifies the actual body of the message|
@@ -184,7 +184,7 @@ end
Just like with controller views, use +yield+ to render the view inside the layout.
-h4. Generating URLs in Action Mailer views
+h4. Generating URLs in Action Mailer Views
URLs can be generated in mailer views using +url_for+ or named routes.
Unlike controllers, the mailer instance doesn't have any context about the incoming request so you'll need to provide the +:host+, +:controller+, and +:action+:
@@ -216,7 +216,7 @@ config.action_mailer.default_url_options = { :host => "example.com" }
If you set a default +:host+ for your mailers you need to pass +:only_path => false+ to +url_for+. Otherwise it doesn't get included.
-h4. Sending multipart emails
+h4. Sending Multipart Emails
Action Mailer will automatically send multipart emails if you have different templates for the same action. So, for our UserMailer example, if you have +welcome_email.text.plain.erb+ and +welcome_email.text.html.erb+ in +app/views/user_mailer+, Action Mailer will automatically send a multipart email with the HTML and text versions setup as different parts.
@@ -240,7 +240,7 @@ class UserMailer < ActionMailer::Base
end
</ruby>
-h4. Sending emails with attachments
+h4. Sending Emails with Attachments
Attachments can be added by using the +attachment+ method:
@@ -262,6 +262,38 @@ class UserMailer < ActionMailer::Base
end
</ruby>
+h4. Sending Multipart Emails with Attachments
+
+Once you use the +attachment+ method, ActionMailer will no longer automagically use the correct template based on the filename. You must declare which template you are using for each content type via the +part+ method.
+
+In the following example, there would be two template files, +welcome_email_html.erb+ and +welcome_email_plain.erb+ in the +app/views/user_mailer+ folder.
+
+<ruby>
+class UserMailer < ActionMailer::Base
+ def welcome_email(user)
+ recipients user.email_address
+ subject "New account information"
+ from "system@example.com"
+ content_type "multipart/alternative"
+
+ part "text/html" do |p|
+ p.body = render_message("welcome_email_html", :message => "<h1>HTML content</h1>")
+ end
+
+ part "text/plain" do |p|
+ p.body = render_message("welcome_email_plain", :message => "text content")
+ end
+
+ attachment :content_type => "image/jpeg",
+ :body => File.read("an-image.jpg")
+
+ attachment "application/pdf" do |a|
+ a.body = generate_your_pdf_here()
+ end
+ end
+end
+</ruby>
+
h3. Receiving Emails
Receiving and parsing emails with Action Mailer can be a rather complex endeavour. Before your email reaches your Rails app, you would have had to configure your system to somehow forward emails to your app, which needs to be listening for that. So, to receive emails in your Rails app you'll need:
@@ -320,7 +352,7 @@ The following configuration options are best made in one of the environment file
|default_implicit_parts_order|When a message is built implicitly (i.e. multiple parts are assembled from templates which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to ["text/html", "text/enriched", "text/plain"]. Items that appear first in the array have higher priority in the mail client and appear last in the mime encoded message. You can also pick a different order from inside a method with implicit_parts_order.|
-h4. Example Action Mailer configuration
+h4. Example Action Mailer Configuration
An example would be:
@@ -352,7 +384,7 @@ ActionMailer::Base.smtp_settings = {
}
</ruby>
-h4. Configure Action Mailer to recognize HAML templates
+h4. Configure Action Mailer to Recognize HAML Templates
In +config/environment.rb+, add the following line:
@@ -386,3 +418,7 @@ end
</ruby>
In the test we send the email and store the returned object in the +email+ variable. We then ensure that it was sent (the first assert), then, in the second batch of assertions, we ensure that the email does indeed contain the what we expect.
+
+h3. Changelog
+
+"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213/tickets/25
diff --git a/railties/guides/source/active_record_basics.textile b/railties/guides/source/active_record_basics.textile
index ed3f6cdd61..afff892fd4 100644
--- a/railties/guides/source/active_record_basics.textile
+++ b/railties/guides/source/active_record_basics.textile
@@ -29,7 +29,7 @@ h3. Object Relational Mapping
The relation between databases and object-oriented software is called ORM, which is short for "Object Relational Mapping". The purpose of an ORM framework is to minimize the mismatch existent between relational databases and object-oriented software. In applications with a domain model, we have objects that represent both the state of the system and the behavior of the real world elements that were modeled through these objects. Since we need to store the system's state somehow, we can use relational databases, which are proven to be an excellent approach to data management. Usually this may become a very hard thing to do, since we need to create an object-oriented model of everything that lives in the database, from simple columns to complicated relations between different tables. Doing this kind of thing by hand is a tedious and error prone job. This is where an ORM framework comes in.
-h3. ActiveRecord as an ORM framework
+h3. ActiveRecord as an ORM Framework
ActiveRecord gives us several mechanisms, being the most important ones the ability to:
@@ -41,7 +41,7 @@ ActiveRecord gives us several mechanisms, being the most important ones the abil
It's easy to see that the Rails Active Record implementation goes way beyond the basic description of the Active Record Pattern.
-h3. Active Record inside the MVC model
+h3. Active Record Inside the MVC Model
Active Record plays the role of model inside the MVC structure followed by Rails applications. Since model objects should encapsulate both state and logic of your applications, it's ActiveRecord responsibility to deliver you the easiest possible way to recover this data from the database.
@@ -81,7 +81,7 @@ There are also some optional column names that will create additional features t
NOTE: While these column names are optional they are in fact reserved by ActiveRecord. Steer clear of reserved keywords unless you want the extra functionality. For example, "type" is a reserved keyword used to designate a table using Single Table Inheritance. If you are not using STI, try an analogous keyword like "context", that may still accurately describe the data you are modeling.
-h3. Creating ActiveRecord models
+h3. Creating ActiveRecord Models
It's very easy to create ActiveRecord models. All you have to do is to subclass the ActiveRecord::Base class and you're good to go:
@@ -107,7 +107,7 @@ p.name = "Some Book"
puts p.name # "Some Book"
</ruby>
-h3. Overriding the naming conventions
+h3. Overriding the Naming Conventions
What if you need to follow a different naming convention or need to use your Rails application with a legacy database? No problem, you can easily override the default conventions.
diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile
index 03e1b264b2..b112c4f5fb 100644
--- a/railties/guides/source/active_record_querying.textile
+++ b/railties/guides/source/active_record_querying.textile
@@ -54,7 +54,7 @@ end
Active Record will perform queries on the database for you and is compatible with most database systems (MySQL, PostgreSQL and SQLite to name a few). Regardless of which database system you're using, the Active Record method format will always be the same.
-h3. Retrieving objects from the database
+h3. Retrieving Objects from the Database
To retrieve objects from the database, Active Record provides a class method called +Model.find+. This method allows you to pass arguments into it to perform certain queries on your database without the need of writing raw SQL.
@@ -65,11 +65,11 @@ Primary operation of <tt>Model.find(options)</tt> can be summarized as:
* Instantiate the equivalent Ruby object of the appropriate model for every resulting row.
* Run +after_find+ callbacks if any.
-h4. Retrieving a single object
+h4. Retrieving a Single Object
Active Record lets you retrieve a single object using three different ways.
-h5. Using a primary key
+h5. Using a Primary Key
Using <tt>Model.find(primary_key, options = nil)</tt>, you can retrieve the object corresponding to the supplied _primary key_ and matching the supplied options (if any). For example:
@@ -87,7 +87,7 @@ SELECT * FROM clients WHERE (clients.id = 10)
<tt>Model.find(primary_key)</tt> will raise an +ActiveRecord::RecordNotFound+ exception if no matching record is found.
-h5. Find first
+h5. +first+
<tt>Model.first(options = nil)</tt> finds the first record matched by the supplied options. If no +options+ are supplied, the first matching record is returned. For example:
@@ -106,12 +106,11 @@ SELECT * FROM clients LIMIT 1
NOTE: +Model.find(:first, options)+ is equivalent to +Model.first(options)+
-h5. Find last
+h5. +last+
<tt>Model.last(options = nil)</tt> finds the last record matched by the supplied options. If no +options+ are supplied, the last matching record is returned. For example:
<ruby>
-# Find the client with primary key (id) 10.
client = Client.last
=> #<Client id: 221, name: => "Russel">
</ruby>
@@ -126,9 +125,9 @@ SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
NOTE: +Model.find(:last, options)+ is equivalent to +Model.last(options)+
-h4. Retrieving multiple objects
+h4. Retrieving Multiple Objects
-h5. Using multiple primary keys
+h5. Using Multiple Primary Keys
<tt>Model.find(array_of_primary_key, options = nil)</tt> also accepts an array of _primary keys_. An array of all the matching records for the supplied _primary keys_ is returned. For example:
@@ -166,17 +165,85 @@ SELECT * FROM clients
NOTE: +Model.find(:all, options)+ is equivalent to +Model.all(options)+
+h4. Retrieving Multiple Objects in Batches
+
+Sometimes you need to iterate over a large set of records. For example to send a newsletter to all users, to export some data, etc.
+
+The following may seem very straight forward at first:
+
+<ruby>
+# Very inefficient when users table has thousands of rows.
+User.all.each do |user|
+ NewsLetter.weekly_deliver(user)
+end
+</ruby>
+
+But if the total number of rows in the table is very large, the above approach may vary from being under performant to just plain impossible.
+
+This is because +User.all+ makes Active Record fetch _the entire table_, build a model object per row, and keep the entire array in the memory. Sometimes that is just too many objects and demands too much memory.
+
+h5. +find_each+
+
+To efficiently iterate over a large table, Active Record provides a batch finder method called +find_each+:
+
+<ruby>
+User.find_each do |user|
+ NewsLetter.weekly_deliver(user)
+end
+</ruby>
+
+*Configuring the batch size*
+
+Behind the scenes +find_each+ fetches rows in batches of +1000+ and yields them one by one. The size of the underlying batches is configurable via the +:batch_size+ option.
+
+To fetch +User+ records in batch size of +5000+:
+
+<ruby>
+User.find_each(:batch_size => 5000) do |user|
+ NewsLetter.weekly_deliver(user)
+end
+</ruby>
+
+*Starting batch find from a specific primary key*
+
+Records are fetched in ascending order on the primary key, which must be an integer. The +:start+ option allows you to configure the first ID of the sequence if the lowest is not the one you need. This may be useful for example to be able to resume an interrupted batch process if it saves the last processed ID as a checkpoint.
+
+To send newsletters only to users with the primary key starting from +2000+:
+
+<ruby>
+User.find_each(:batch_size => 5000, :start => 2000) do |user|
+ NewsLetter.weekly_deliver(user)
+end
+</ruby>
+
+*Additional options*
+
++find_each+ accepts the same options as the regular +find+ method. However, +:order+ and +:limit+ are needed internally and hence not allowed to be passed explicitly.
+
+h5. +find_in_batches+
+
+You can also work by chunks instead of row by row using +find_in_batches+. This method is analogous to +find_each+, but it yields arrays of models instead:
+
+<ruby>
+# Works in chunks of 1000 invoices at a time.
+Invoice.find_in_batches(:include => :invoice_lines) do |invoices|
+ export.add_invoices(invoices)
+end
+</ruby>
+
+The above will yield the supplied block with +1000+ invoices every time.
+
h3. Conditions
-The +find+ method allows you to specify conditions to limit the records returned, representing the WHERE-part of the SQL statement. Conditions can either be specified as a string, array, or hash.
+The +find+ method allows you to specify conditions to limit the records returned, representing the +WHERE+-part of the SQL statement. Conditions can either be specified as a string, array, or hash.
-h4. Pure string conditions
+h4. Pure String Conditions
If you'd like to add conditions to your find, you could just specify them in there, just like +Client.first(:conditions => "orders_count = '2'")+. This will find all clients where the +orders_count+ field's value is 2.
WARNING: Building your own conditions as pure strings can leave you vulnerable to SQL injection exploits. For example, +Client.first(:conditions => "name LIKE '%#{params[:name]}%'")+ is not safe. See the next section for the preferred way to handle conditions using an array.
-h4. Array conditions
+h4. Array Conditions
Now what if that number could vary, say as a argument from somewhere, or perhaps from the user's level status somewhere? The find then becomes something like:
@@ -208,11 +275,11 @@ Client.first(:conditions => "orders_count = #{params[:orders]}")
is because of argument safety. Putting the variable directly into the conditions string will pass the variable to the database *as-is*. This means that it will be an unescaped variable directly from a user who may have malicious intent. If you do this, you put your entire database at risk because once a user finds out he or she can exploit your database they can do just about anything to it. Never ever put your arguments directly inside the conditions string.
-TIP: For more information on the dangers of SQL injection, see the "Ruby on Rails Security Guide":../security.html#_sql_injection.
+TIP: For more information on the dangers of SQL injection, see the "Ruby on Rails Security Guide":security.html#sql-injection.
-h5. Placeholder conditions
+h5. Placeholder Conditions
-Similar to the +(?)+ replacement style of params, you can also specify keys/values hash in your Array conditions:
+Similar to the +(?)+ replacement style of params, you can also specify keys/values hash in your array conditions:
<ruby>
Client.all(:conditions =>
@@ -221,7 +288,7 @@ Client.all(:conditions =>
This makes for clearer readability if you have a large number of variable conditions.
-h5. Range conditions
+h5. Range Conditions
If you're looking for a range inside of a table (for example, users created in a certain timeframe) you can use the conditions option coupled with the +IN+ SQL statement for this. If you had two dates coming in from a controller you could do something like this to look for a range:
@@ -243,7 +310,7 @@ SELECT * FROM users WHERE (created_at IN
'2008-12-27','2008-12-28','2008-12-29','2008-12-30','2008-12-31'))
</sql>
-h5. Time and Date conditions
+h5. Time and Date Conditions
Things can get *really* messy if you pass in Time objects as it will attempt to compare your field to *every second* in that range:
@@ -280,15 +347,15 @@ Client.all(:conditions =>
["created_at >= ? AND created_at <= ?", params[:start_date], params[:end_date]])
</ruby>
-Just like in Ruby. If you want a shorter syntax be sure to check out the "Hash Conditions":hash-conditions section later on in the guide.
+Just like in Ruby. If you want a shorter syntax be sure to check out the "Hash Conditions":#hash-conditions section later on in the guide.
-h4. Hash conditions
+h4. Hash Conditions
Active Record also allows you to pass in a hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want conditionalised and the values of how you want to conditionalise them:
NOTE: Only equality, range and subset checking are possible with Hash conditions.
-h5. Equality conditions
+h5. Equality Conditions
<ruby>
Client.all(:conditions => { :locked => true })
@@ -300,7 +367,7 @@ The field name does not have to be a symbol it can also be a string:
Client.all(:conditions => { 'locked' => true })
</ruby>
-h5. Range conditions
+h5. Range Conditions
The good thing about this is that we can pass in a range for our fields without it generating a large query as shown in the preamble of this section.
@@ -314,9 +381,9 @@ This will find all clients created yesterday by using a +BETWEEN+ SQL statement:
SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
</sql>
-This demonstrates a shorter syntax for the examples in "Array Conditions":#arrayconditions
+This demonstrates a shorter syntax for the examples in "Array Conditions":#array-conditions
-h5. Subset conditions
+h5. Subset Conditions
If you want to find records using the +IN+ expression you can pass an array to the conditions hash:
@@ -330,7 +397,7 @@ This code will generate SQL like this:
SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
</sql>
-h3. Find options
+h3. Find Options
Apart from +:conditions+, +Model.find+ takes a variety of other options via the options hash for customizing the resulting record set.
@@ -370,13 +437,13 @@ Or ordering by multiple fields:
Client.all(:order => "orders_count ASC, created_at DESC")
</ruby>
-h4. Selecting specific fields
+h4. Selecting Specific Fields
By default, <tt>Model.find</tt> selects all the fields from the result set using +select *+.
To select only a subset of fields from the result set, you can specify the subset via +:select+ option on the +find+.
-NOTE: If the +:select+ option is used, all the returning objects will be "read only":#readonlyobjects.
+NOTE: If the +:select+ option is used, all the returning objects will be "read only":#readonly-objects.
<br />
@@ -470,7 +537,7 @@ SELECT * FROM orders GROUP BY date(created_at) HAVING created_at > '2009-01-15'
This will return single order objects for each day, but only for the last month.
-h4. Readonly objects
+h4. Readonly Objects
To explicitly disallow modification/destroyal of the matching records returned by +Model.find+, you could specify the +:readonly+ option as +true+ to the find call.
@@ -488,7 +555,7 @@ client.locked = false
client.save
</ruby>
-h4. Locking records for update
+h4. Locking Records for Update
Locking is helpful for preventing the race conditions when updating records in the database and ensuring atomic updated. Active Record provides two locking mechanism:
@@ -562,31 +629,31 @@ Item.transaction do
end
</ruby>
-h3. Joining tables
+h3. Joining Tables
<tt>Model.find</tt> provides a +:joins+ option for specifying +JOIN+ clauses on the resulting SQL. There multiple different ways to specify the +:joins+ option:
-h4. Using a string SQL fragment
+h4. Using a String SQL Fragment
You can just supply the raw SQL specifying the +JOIN+ clause to the +:joins+ option. For example:
<ruby>
-Client.all(:joins => 'LEFT OUTER JOIN addresses ON addresses.client_id = client.id')
+Client.all(:joins => 'LEFT OUTER JOIN addresses ON addresses.client_id = clients.id')
</ruby>
This will result in the following SQL:
<sql>
-SELECT clients.* FROM clients INNER JOIN addresses ON addresses.client_id = clients.id
+SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id = clients.id
</sql>
-h4. Using Array/Hash of named associations
+h4. Using Array/Hash of Named Associations
WARNING: This method only works with +INNER JOIN+,
<br />
-Active Record lets you use the names of the "associations":association_basics.html defined on the Model, as a shortcut for specifying the +:joins+ option.
+Active Record lets you use the names of the "associations":association_basics.html defined on the model as a shortcut for specifying the +:joins+ option.
For example, consider the following +Category+, +Post+, +Comments+ and +Guest+ models:
@@ -613,7 +680,7 @@ end
Now all of the following will produce the expected join queries using +INNER JOIN+:
-h5. Joining a single association
+h5. Joining a Single Association
<ruby>
Category.all :joins => :posts
@@ -626,7 +693,7 @@ SELECT categories.* FROM categories
INNER JOIN posts ON posts.category_id = categories.id
</sql>
-h5. Joining multiple associations
+h5. Joining Multiple Associations
<ruby>
Post.all :joins => [:category, :comments]
@@ -640,21 +707,21 @@ SELECT posts.* FROM posts
INNER JOIN comments ON comments.post_id = posts.id
</sql>
-h5. Joining nested associations (single level)
+h5. Joining Nested Associations (Single Level)
<ruby>
Post.all :joins => {:comments => :guest}
</ruby>
-h5. Joining nested associations (multiple level)
+h5. Joining Nested Associations (Multiple Level)
<ruby>
Category.all :joins => {:posts => [{:comments => :guest}, :tags]}
</ruby>
-h4. Specifying conditions on the joined tables
+h4. Specifying Conditions on the Joined Tables
-You can specify conditions on the joined tables using the regular "Array":#arrayconditions and "String":#purestringconditions conditions. "Hash conditions":#hashconditions provides a special syntax for specifying conditions for the joined tables:
+You can specify conditions on the joined tables using the regular "Array":#array-conditions and "String":#pure-string-conditions conditions. "Hash conditions":#hash-conditions provides a special syntax for specifying conditions for the joined tables:
<ruby>
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
@@ -670,7 +737,7 @@ Client.all :joins => :orders, :conditions => {:orders => {:created_at => time_ra
This will find all clients who have orders that were created yesterday, again using a +BETWEEN+ SQL expression.
-h3. Eager loading associations
+h3. Eager Loading Associations
Eager loading is the mechanism for loading the associated records of the objects returned by +Model.find+ using as few queries as possible.
@@ -710,11 +777,11 @@ SELECT addresses.* FROM addresses
WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
</sql>
-h4. Eager loading multiple associations
+h4. Eager Loading Multiple Associations
-Active Record lets you eager load any possible number of associations with a single +Model.find+ call by using Array, Hash or a nested Hash of Array/Hash with +:include+ find option.
+Active Record lets you eager load any possible number of associations with a single +Model.find+ call by using an array, hash, or a nested hash of array/hash with the +:include+ option.
-h5. Array of multiple associations
+h5. Array of Multiple Associations
<ruby>
Post.all :include => [:category, :comments]
@@ -722,7 +789,7 @@ Post.all :include => [:category, :comments]
This loads all the posts and the associated category and comments for each post.
-h5. Nested assocaitions hash
+h5. Nested Associations Hash
<ruby>
Category.find 1, :include => {:posts => [{:comments => :guest}, :tags]}
@@ -730,17 +797,17 @@ Category.find 1, :include => {:posts => [{:comments => :guest}, :tags]}
The above code finds the category with id 1 and eager loads all the posts associated with the found category. Additionally, it will also eager load every posts' tags and comments. Every comment's guest association will get eager loaded as well.
-h4. Specifying conditions on eager loaded associations
+h4. Specifying Conditions on Eager Loaded Associations
-Even though Active Record lets you specify conditions on the eager loaded associations just like +:joins+, the recommended way is to use ":joins":#joiningtables instead.
+Even though Active Record lets you specify conditions on the eager loaded associations just like +:joins+, the recommended way is to use ":joins":#joining-tables instead.
-h3. Dynamic finders
+h3. Dynamic Finders
-For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called +name+ on your Client model for example, you get +find_by_name+ and +find_all_by_name+ for free from Active Record. If you have also have a +locked+ field on the Client model, you also get +find_by_locked+ and +find_all_by_locked+.
+For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called +name+ on your +Client+ model for example, you get +find_by_name+ and +find_all_by_name+ for free from Active Record. If you have also have a +locked+ field on the +Client+ model, you also get +find_by_locked+ and +find_all_by_locked+.
You can do +find_last_by_*+ methods too which will find the last record matching your argument.
-You can specify an exclamation point (!) on the end of the dynamic finders to get them to raise an ActiveRecord::RecordNotFound error if they do not return any records, like +Client.find_by_name!("Ryan")+
+You can specify an exclamation point (!) on the end of the dynamic finders to get them to raise an +ActiveRecord::RecordNotFound+ error if they do not return any records, like +Client.find_by_name!("Ryan")+
If you want to find both by name and locked, you can chain these finders together by simply typing +and+ between the fields for example +Client.find_by_name_and_locked("Ryan", true)+.
@@ -761,9 +828,9 @@ COMMIT
client = Client.find_or_initialize_by_name('Ryan')
</ruby>
-will either assign an existing client object with the name 'Ryan' to the client local variable, or initialize a new object similar to calling +Client.new(:name => 'Ryan')+. From here, you can modify other fields in client by calling the attribute setters on it: +client.locked = true+ and when you want to write it to the database just call +save+ on it.
+will either assign an existing client object with the name "Ryan" to the client local variable, or initialize a new object similar to calling +Client.new(:name => 'Ryan')+. From here, you can modify other fields in client by calling the attribute setters on it: +client.locked = true+ and when you want to write it to the database just call +save+ on it.
-h3. Finding By SQL
+h3. Finding by SQL
If you'd like to use your own SQL to find records in a table you can use +find_by_sql+. The +find_by_sql+ method will return an array of objects even the underlying query returns just a single record. For example you could run this query:
@@ -775,7 +842,7 @@ Client.find_by_sql("SELECT * FROM clients
+find_by_sql+ provides you with a simple way of making custom calls to the database and retrieving instantiated objects.
-h3. select_all
+h3. +select_all+
<tt>find_by_sql</tt> has a close relative called +connection#select_all+. +select_all+ will retrieve objects from the database using custom SQL just like +find_by_sql+ but will not instantiate them. Instead, you will get an array of hashes where each hash indicates a record.
diff --git a/railties/guides/source/activerecord_validations_callbacks.textile b/railties/guides/source/activerecord_validations_callbacks.textile
index 01e52bf01e..5ae4884297 100644
--- a/railties/guides/source/activerecord_validations_callbacks.textile
+++ b/railties/guides/source/activerecord_validations_callbacks.textile
@@ -16,7 +16,7 @@ endprologue.
h3. The Object Lifecycle
-During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this <em>object lifecycle</em> so that you can control your application and its data.
+During the normal operation of a Rails application objects may be created, updated, and destroyed. Active Record provides hooks into this <em>object lifecycle</em> so that you can control your application and its data.
Validations allow you to ensure that only valid data is stored in your database. Callbacks and observers allow you to trigger logic before or after an alteration of an object's state.
@@ -26,18 +26,18 @@ Before you dive into the detail of validations in Rails, you should understand a
h4. Why Use Validations?
-Validations are used to ensure that only valid data is saved into your database. For example, it may be important to your application to ensure that every user provides a valid email address and mailing address
+Validations are used to ensure that only valid data is saved into your database. For example, it may be important to your application to ensure that every user provides a valid email address and mailing address.
There are several ways to validate data before it is saved into your database, including native database constraints, client-side validations, controller-level validations, and model-level validations.
* Database constraints and/or stored procedures make the validation mechanisms database-dependent and can make testing and maintenance more difficult. However, if your database is used by other applications, it may be a good idea to use some constraints at the database level. Additionally, database-level validations can safely handle some things (such as uniqueness in heavily-used tables) that can be difficult to implement otherwise.
-* Client-side validations can be useful, but are generally unreliable if used alone. If they are implemented using Javascript, they may be bypassed if Javascript is turned off in the user's browser. However, if combined with other techniques, client-side validation can be a convenient way to provide users with immediate feedback as they use your site.
+* Client-side validations can be useful, but are generally unreliable if used alone. If they are implemented using JavaScript, they may be bypassed if JavaScript is turned off in the user's browser. However, if combined with other techniques, client-side validation can be a convenient way to provide users with immediate feedback as they use your site.
* Controller-level validations can be tempting to use, but often become unwieldy and difficult to test and maintain. Whenever possible, it's a good idea to "keep your controllers skinny":http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model, as it will make your application a pleasure to work with in the long run.
* Model-level validations are the best way to ensure that only valid data is saved into your database. They are database agnostic, cannot be bypassed by end users, and are convenient to test and maintain. Rails makes them easy to use, provides built-in helpers for common needs, and allows you to create your own validation methods as well.
h4. When Does Validation Happen?
-There are two kinds of Active Record objects: those that correspond to a row inside your database and those that do not. When you create a fresh object, using the +new+ method, that object does not belong to the database yet. Once you call +save+ upon that object it will be saved into the appropriate database table. Active Record uses the +new_record?+ instance method to determine whether an object is already in the database or not. Consider the following simple Active Record class:
+There are two kinds of Active Record objects: those that correspond to a row inside your database and those that do not. When you create a fresh object, for example using the +new+ method, that object does not belong to the database yet. Once you call +save+ upon that object it will be saved into the appropriate database table. Active Record uses the +new_record?+ instance method to determine whether an object is already in the database or not. Consider the following simple Active Record class:
<ruby>
class Person < ActiveRecord::Base
@@ -57,11 +57,11 @@ We can see how it works by looking at some script/console output:
=> false
</shell>
-Creating and saving a new record will send an SQL +INSERT+ operation to the database. Updating an existing record will send an SQL +UPDATE+ operation instead. Validations are typically run before these commands are sent to the database. If any validations fail, the object will be marked as invalid and Active Record will not trigger the +INSERT+ or +UPDATE+ operation. This helps to avoid storing an object in the database that's invalid. You can choose to have specific validations run when an object is created, saved, or updated.
+Creating and saving a new record will send an SQL +INSERT+ operation to the database. Updating an existing record will send an SQL +UPDATE+ operation instead. Validations are typically run before these commands are sent to the database. If any validations fail, the object will be marked as invalid and Active Record will not perform the +INSERT+ or +UPDATE+ operation. This helps to avoid storing an invalid object in the database. You can choose to have specific validations run when an object is created, saved, or updated.
CAUTION: There are many ways to change the state of an object in the database. Some methods will trigger validations, but some will not. This means that it's possible to save an object in the database in an invalid state if you aren't careful.
-The following methods trigger validations, and will save the object to the database only if the object is valid. The bang versions (e.g. +save!+) will raise an exception if the record is invalid. The non-bang versions (e.g. +save+) simply return +false+.
+The following methods trigger validations, and will save the object to the database only if the object is valid:
* +create+
* +create!+
@@ -71,6 +71,8 @@ The following methods trigger validations, and will save the object to the datab
* +update_attributes+
* +update_attributes!+
+The bang versions (e.g. +save!+) raise an exception if the record is invalid. The non-bang versions don't: +save+ and +update_attributes+ return +false+, +create+ and +update+ just return the object/s.
+
h4. Skipping Validations
The following methods skip validations, and will save the object to the database regardless of its validity. They should be used with caution.
@@ -84,11 +86,11 @@ The following methods skip validations, and will save the object to the database
* +update_attribute+
* +update_counters+
-Note that +save+ also has the ability to skip validations (and callbacks!) if passed +false+. This technique should be used with caution.
+Note that +save+ also has the ability to skip validations if passed +false+ as argument. This technique should be used with caution.
* +save(false)+
-h4. Object#valid? and Object#invalid?
+h4. +valid?+ and +invalid?+
To verify whether or not an object is valid, Rails uses the +valid?+ method. You can also use this method on your own. +valid?+ triggers your validations and returns true if no errors were added to the object, and false otherwise.
@@ -101,7 +103,7 @@ Person.create(:name => "John Doe").valid? # => true
Person.create(:name => nil).valid? # => false
</ruby>
-When Active Record is performing validations, any errors found are collected into an +errors+ instance variable and can be accessed through an +errors+ instance method. An object is considered invalid if it has errors, and calling +save+ or +save!+ will not save it to the database.
+When Active Record is performing validations, any errors found can be accessed through the +errors+ instance method. By definition an object is valid if this collection is empty after running validations.
Note that an object instantiated with +new+ will not report errors even if it's technically invalid, because validations are not run when using +new+.
@@ -135,7 +137,11 @@ end
=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
</ruby>
-To verify whether or not a particular attribute of an object is valid, you can use the +invalid?+ method. This method is only useful _after_ validations have been run, because it only inspects the errors collection and does not trigger validations itself. It's different from the +valid?+ method because it doesn't verify the validity of the object as a whole, but only if there are errors found on an individual attribute of the object.
++invalid?+ is simply the inverse of +valid?+. +invalid?+ triggers your validations and returns true if any errors were added to the object, and false otherwise.
+
+h4. +errors.invalid?+
+
+To verify whether or not a particular attribute of an object is valid, you can use the +errors.invalid?+ method. This method is only useful _after_ validations have been run, because it only inspects the errors collection and does not trigger validations itself. It's different from the +ActiveRecord::Base#invalid?+ method explained above because it doesn't verify the validity of the object as a whole. It only checks to see whether there are errors found on an individual attribute of the object.
<ruby>
class Person < ActiveRecord::Base
@@ -146,19 +152,19 @@ end
>> Person.create.errors.invalid?(:name) # => true
</ruby>
-We'll cover validation errors in greater depth in the *Working with Validation Errors* section. For now, let's turn to the built-in validation helpers that Rails provides by default.
+We'll cover validation errors in greater depth in the "Working with Validation Errors":#working-with-validation-errors section. For now, let's turn to the built-in validation helpers that Rails provides by default.
h3. Validation Helpers
-Active Record offers many pre-defined validation helpers that you can use directly inside your class definitions. These helpers create validation rules that are commonly used. Every time a validation fails, an error message is added to the object's +errors+ collection, and this message is associated with the field being validated.
+Active Record offers many pre-defined validation helpers that you can use directly inside your class definitions. These helpers provide common validation rules. Every time a validation fails, an error message is added to the object's +errors+ collection, and this message is associated with the field being validated.
-Each helper accepts an arbitrary number of attributes identified by symbols, so with a single line of code you can add the same kind of validation to several attributes.
+Each helper accepts an arbitrary number of attribute names, so with a single line of code you can add the same kind of validation to several attributes.
-All these helpers accept the +:on+ and +:message+ options, which define when the validation should be applied and what message should be added to the +errors+ collection when it fails, respectively. The +:on+ option takes one of the values +:save+ (the default), +:create+ or +:update+. There is a default error message for each one of the validation helpers. These messages are used when the +:message+ option isn't used. Let's take a look at each one of the available helpers.
+All of them accept the +:on+ and +:message+ options, which define when the validation should be run and what message should be added to the +errors+ collection if it fails, respectively. The +:on+ option takes one of the values +:save+ (the default), +:create+ or +:update+. There is a default error message for each one of the validation helpers. These messages are used when the +:message+ option isn't specified. Let's take a look at each one of the available helpers.
-h4. validates_acceptance_of
+h4. +validates_acceptance_of+
-Validates that a checkbox on the user interface was checked when a form was submitted. This is normally used when the user needs to agree to your application's terms of service, confirm reading some text, or any similar concept. This validation is very specific to web applications and actually this 'acceptance' does not need to be recorded anywhere in your database (if you don't have a field for it, the helper will just create a virtual attribute).
+Validates that a checkbox on the user interface was checked when a form was submitted. This is typically used when the user needs to agree to your application's terms of service, confirm reading some text, or any similar concept. This validation is very specific to web applications and this 'acceptance' does not need to be recorded anywhere in your database (if you don't have a field for it, the helper will just create a virtual attribute).
<ruby>
class Person < ActiveRecord::Base
@@ -166,7 +172,7 @@ class Person < ActiveRecord::Base
end
</ruby>
-The default error message for +validates_acceptance_of+ is "_must be accepted_"
+The default error message for +validates_acceptance_of+ is "_must be accepted_".
+validates_acceptance_of+ can receive an +:accept+ option, which determines the value that will be considered acceptance. It defaults to "1", but you can change this.
@@ -176,7 +182,7 @@ class Person < ActiveRecord::Base
end
</ruby>
-h4. validates_associated
+h4. +validates_associated+
You should use this helper when your model has associations with other models and they also need to be validated. When you try to save your object, +valid?+ will be called upon each one of the associated objects.
@@ -187,15 +193,15 @@ class Library < ActiveRecord::Base
end
</ruby>
-This validation will work with all the association types.
+This validation will work with all of the association types.
-CAUTION: Don't use +validates_associated+ on both ends of your associations, because this will lead to several recursive calls and blow up the method calls' stack.
+CAUTION: Don't use +validates_associated+ on both ends of your associations, they would call each other in an infinite loop.
The default error message for +validates_associated+ is "_is invalid_". Note that each associated object will contain its own +errors+ collection; errors do not bubble up to the calling model.
-h4. validates_confirmation_of
+h4. +validates_confirmation_of+
-You should use this helper when you have two text fields that should receive exactly the same content. For example, you may want to confirm an email address or a password. This validation creates a virtual attribute, using the name of the field that has to be confirmed with '_confirmation' appended.
+You should use this helper when you have two text fields that should receive exactly the same content. For example, you may want to confirm an email address or a password. This validation creates a virtual attribute whose name is the name of the field that has to be confirmed with "_confirmation" appended.
<ruby>
class Person < ActiveRecord::Base
@@ -210,7 +216,7 @@ In your view template you could use something like
<%= text_field :person, :email_confirmation %>
</erb>
-NOTE: This check is performed only if +email_confirmation+ is not nil, and by default only on save. To require confirmation, make sure to add a presence check for the confirmation attribute (we'll take a look at +validates_presence_of+ later on this guide):
+This check is performed only if +email_confirmation+ is not +nil+. To require confirmation, make sure to add a presence check for the confirmation attribute (we'll take a look at +validates_presence_of+ later on this guide):
<ruby>
class Person < ActiveRecord::Base
@@ -219,54 +225,54 @@ class Person < ActiveRecord::Base
end
</ruby>
-The default error message for +validates_confirmation_of+ is "_doesn't match confirmation_"
+The default error message for +validates_confirmation_of+ is "_doesn't match confirmation_".
-h4. validates_exclusion_of
+h4. +validates_exclusion_of+
This helper validates that the attributes' values are not included in a given set. In fact, this set can be any enumerable object.
<ruby>
-class MovieFile < ActiveRecord::Base
- validates_exclusion_of :format, :in => %w(mov avi),
- :message => "Extension %s is not allowed"
+class Account < ActiveRecord::Base
+ validates_exclusion_of :subdomain, :in => %w(www),
+ :message => "Subdomain {{value}} is reserved."
end
</ruby>
-The +validates_exclusion_of+ helper has an option +:in+ that receives the set of values that will not be accepted for the validated attributes. The +:in+ option has an alias called +:within+ that you can use for the same purpose, if you'd like to. This example uses the +:message+ option to show how you can personalize it with the current attribute's value, through the +%s+ format mask.
+The +validates_exclusion_of+ helper has an option +:in+ that receives the set of values that will not be accepted for the validated attributes. The +:in+ option has an alias called +:within+ that you can use for the same purpose, if you'd like to. This example uses the +:message+ option to show how you can include the attribute's value.
The default error message for +validates_exclusion_of+ is "_is not included in the list_".
-h4. validates_format_of
+h4. +validates_format_of+
-This helper validates the attributes' values by testing whether they match a given pattern. This pattern must be specified using a Ruby regular expression, which is specified using the +:with+ option.
+This helper validates the attributes' values by testing whether they match a given regular expresion, which is specified using the +:with+ option.
<ruby>
class Product < ActiveRecord::Base
- validates_format_of :description, :with => /^[a-zA-Z]+$/,
+ validates_format_of :legacy_code, :with => /\A[a-zA-Z]+\z/,
:message => "Only letters allowed"
end
</ruby>
The default error message for +validates_format_of+ is "_is invalid_".
-h4. validates_inclusion_of
+h4. +validates_inclusion_of+
This helper validates that the attributes' values are included in a given set. In fact, this set can be any enumerable object.
<ruby>
class Coffee < ActiveRecord::Base
validates_inclusion_of :size, :in => %w(small medium large),
- :message => "%s is not a valid size"
+ :message => "{{value}} is not a valid size"
end
</ruby>
-The +validates_inclusion_of+ helper has an option +:in+ that receives the set of values that will be accepted. The +:in+ option has an alias called +:within+ that you can use for the same purpose, if you'd like to. The previous example uses the +:message+ option to show how you can personalize it with the current attribute's value, through the +%s+ format mask.
+The +validates_inclusion_of+ helper has an option +:in+ that receives the set of values that will be accepted. The +:in+ option has an alias called +:within+ that you can use for the same purpose, if you'd like to. The previous example uses the +:message+ option to show how you can include the attribute's value.
The default error message for +validates_inclusion_of+ is "_is not included in the list_".
-h4. validates_length_of
+h4. +validates_length_of+
-This helper validates the length of your attribute's value. It includes a variety of different options, so you can specify length constraints in different ways:
+This helper validates the length of the attributes' values. It provides a variety of options, so you can specify length constraints in different ways:
<ruby>
class Person < ActiveRecord::Base
@@ -281,24 +287,46 @@ The possible length constraint options are:
* +:minimum+ - The attribute cannot have less than the specified length.
* +:maximum+ - The attribute cannot have more than the specified length.
-* +:in+ (or +:within+) - The attribute length must be included in a given interval. The value for this option must be a Ruby range.
-* +:is+ - The attribute length must be equal to a given value.
+* +:in+ (or +:within+) - The attribute length must be included in a given interval. The value for this option must be a range.
+* +:is+ - The attribute length must be equal to the given value.
-The default error messages depend on the type of length validation being performed. You can personalize these messages, using the +:wrong_length+, +:too_long+ and +:too_short+ options and the +%d+ format mask as a placeholder for the number corresponding to the length constraint being used. You can still use the +:message+ option to specify an error message.
+The default error messages depend on the type of length validation being performed. You can personalize these messages using the +:wrong_length+, +:too_long+, and +:too_short+ options and <tt>{{count}}</tt> as a placeholder for the number corresponding to the length constraint being used. You can still use the +:message+ option to specify an error message.
<ruby>
class Person < ActiveRecord::Base
- validates_length_of :bio, :too_long => "you're writing too much. %d characters is the maximum allowed."
+ validates_length_of :bio, :maximum => 1000,
+ :too_long => "{{count}} characters is the maximum allowed"
+end
+</ruby>
+
+This helper counts characters by default, but you can split the value in a different way using the +:tokenizer+ option:
+
+<ruby>
+class Essay < ActiveRecord::Base
+ validates_length_of :content,
+ :minimum => 300,
+ :maximum => 400,
+ :tokenizer => lambda { |str| str.scan(/\w+/) },
+ :too_short => "must have at least {{count}} words",
+ :too_long => "must have at most {{count}} words"
end
</ruby>
The +validates_size_of+ helper is an alias for +validates_length_of+.
-h4. validates_numericality_of
+h4. +validates_numericality_of+
-This helper validates that your attributes have only numeric values. By default, it will match an optional sign followed by a integral or floating point number. Using the +:integer_only+ option set to true, you can specify that only integral numbers are allowed.
+This helper validates that your attributes have only numeric values. By default, it will match an optional sign followed by an integral or floating point number. To specify that only integral numbers are allowed set +:only_integer+ to true.
-If you set +:integer_only+ to +true+, then it will use the +$$/\A[+\-]?\d+\Z/+ regular expression to validate the attribute's value. Otherwise, it will try to convert the value to a number using +Kernel.Float+.
+If you set +:only_integer+ to +true+, then it will use the
+
+<ruby>
+/\A[+-]?\d+\Z/
+</ruby>
+
+regular expression to validate the attribute's value. Otherwise, it will try to convert the value to a number using +Float+.
+
+WARNING. Note that the regular expression above allows a trailing newline character.
<ruby>
class Player < ActiveRecord::Base
@@ -309,19 +337,19 @@ end
Besides +:only_integer+, the +validates_numericality_of+ helper also accepts the following options to add constraints to acceptable values:
-* +:greater_than+ - Specifies the value must be greater than the supplied value. The default error message for this option is "_must be greater than (value)_"
-* +:greater_than_or_equal_to+ - Specifies the value must be greater than or equal the supplied value. The default error message for this option is "_must be greater than or equal to (value)_"
-* +:equal_to+ - Specifies the value must be equal to the supplied value. The default error message for this option is "_must be equal to (value)_"
-* +:less_than+ - Specifies the value must be less than the supplied value. The default error message for this option is "_must e less than (value)_"
-* +:less_than_or_equal_to+ - Specifies the value must be less than or equal the supplied value. The default error message for this option is "_must be less or equal to (value)_"
-* +:odd+ - Specifies the value must be an odd number if set to true. The default error message for this option is "_must be odd_"
-* +:even+ - Specifies the value must be an even number if set to true. The default error message for this option is "_must be even_"
+* +:greater_than+ - Specifies the value must be greater than the supplied value. The default error message for this option is "_must be greater than {{count}}_".
+* +:greater_than_or_equal_to+ - Specifies the value must be greater than or equal to the supplied value. The default error message for this option is "_must be greater than or equal to {{count}}".
+* +:equal_to+ - Specifies the value must be equal to the supplied value. The default error message for this option is "_must be equal to {{count}}_".
+* +:less_than+ - Specifies the value must be less than the supplied value. The default error message for this option is "_must be less than {{count}}_".
+* +:less_than_or_equal_to+ - Specifies the value must be less than or equal the supplied value. The default error message for this option is "_must be less or equal to {{count}}_".
+* +:odd+ - Specifies the value must be an odd number if set to true. The default error message for this option is "_must be odd_".
+* +:even+ - Specifies the value must be an even number if set to true. The default error message for this option is "_must be even_".
The default error message for +validates_numericality_of+ is "_is not a number_".
-h4. validates_presence_of
+h4. +validates_presence_of+
-This helper validates that the specified attributes are not empty. It uses the +blank?+ method to check if the value is either +nil+ or an empty string (if the string has only spaces, it will still be considered empty).
+This helper validates that the specified attributes are not empty. It uses the +blank?+ method to check if the value is either +nil+ or a blank string, that is, a string that is either empty or consists of whitespace.
<ruby>
class Person < ActiveRecord::Base
@@ -338,13 +366,13 @@ class LineItem < ActiveRecord::Base
end
</ruby>
-If you want to validate the presence of a boolean field (where the real values are true and false), you should use +validates_inclusion_of :field_name, :in => [true, false]+. This is due to the way that +Object#blank?+ handles boolean values (+false.blank? # => true+).
+Since +false.blank?+ is true, if you want to validate the presence of a boolean field you should use +validates_inclusion_of :field_name, :in => [true, false]+.
The default error message for +validates_presence_of+ is "_can't be empty_".
-h4. validates_uniqueness_of
+h4. +validates_uniqueness_of+
-This helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint directly into your database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, you must create an unique index in your database.
+This helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, you must create an unique index in your database.
<ruby>
class Account < ActiveRecord::Base
@@ -352,14 +380,14 @@ class Account < ActiveRecord::Base
end
</ruby>
-The validation happens by performing a SQL query into the model's table, searching for a record where the attribute that must be validated is equal to the value in the object being validated.
+The validation happens by performing a SQL query into the model's table, searching for an existing record with the same value in that attribute.
There is a +:scope+ option that you can use to specify other attributes that are used to limit the uniqueness check:
<ruby>
class Holiday < ActiveRecord::Base
validates_uniqueness_of :name, :scope => :year,
- :message => "Should happen once per year"
+ :message => "should happen once per year"
end
</ruby>
@@ -371,16 +399,18 @@ class Person < ActiveRecord::Base
end
</ruby>
+WARNING. Note that some databases are configured to perform case-insensitive searches anyway.
+
The default error message for +validates_uniqueness_of+ is "_has already been taken_".
-h4. validates_each
+h4. +validates_each+
This helper validates attributes against a block. It doesn't have a predefined validation function. You should create one using a block, and every attribute passed to +validates_each+ will be tested against it. In the following example, we don't want names and surnames to begin with lower case.
<ruby>
class Person < ActiveRecord::Base
validates_each :name, :surname do |model, attr, value|
- model.errors.add(attr, 'Must start with upper case') if value =~ /^[a-z]/
+ model.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/
end
end
</ruby>
@@ -389,22 +419,22 @@ The block receives the model, the attribute's name and the attribute's value. Yo
h3. Common Validation Options
-There are some common options that all the validation helpers can use. Here they are, except for the +:if+ and +:unless+ options, which are discussed later in the conditional validation topic.
+There are some common options that all the validation helpers can use. Here they are, except for the +:if+ and +:unless+ options, which are discussed later in "Conditional Validation":#conditional-validation.
-h4. :allow_nil
+h4. +:allow_nil+
-The +:allow_nil+ option skips the validation when the value being validated is +nil+. You may be asking yourself if it makes any sense to use +:allow_nil+ and +validates_presence_of+ together. Well, it does. Remember, the validation will be skipped only for +nil+ attributes, but empty strings are not considered +nil+.
+The +:allow_nil+ option skips the validation when the value being validated is +nil+. Using +:allow_nil+ with +validates_presence_of+ allows for +nil+, but any other +blank?+ value will still be rejected.
<ruby>
class Coffee < ActiveRecord::Base
validates_inclusion_of :size, :in => %w(small medium large),
- :message => "%s is not a valid size", :allow_nil => true
+ :message => "{{value}} is not a valid size", :allow_nil => true
end
</ruby>
-h4. :allow_blank
+h4. +:allow_blank+
-The +:allow_blank+ option is similar to the +:allow_nil+ option. This option will let validation pass if the attribute's value is +nil+ or an empty string, i.e., any value that returns +true+ for +blank?+.
+The +:allow_blank+ option is similar to the +:allow_nil+ option. This option will let validation pass if the attribute's value is +blank?+, like +nil+ or an empty string for example.
<ruby>
class Topic < ActiveRecord::Base
@@ -415,32 +445,32 @@ Topic.create("title" => "").valid? # => true
Topic.create("title" => nil).valid? # => true
</ruby>
-h4. :message
+h4. +:message+
-As you've already seen, the +:message+ option lets you specify the message that will be added to the +errors+ collection when validation fails. When this option is not used, Active Record will use the respective default error message for each validation helper, together with the attribute name.
+As you've already seen, the +:message+ option lets you specify the message that will be added to the +errors+ collection when validation fails. When this option is not used, Active Record will use the respective default error message for each validation helper.
-h4. :on
+h4. +:on+
The +:on+ option lets you specify when the validation should happen. The default behavior for all the built-in validation helpers is to be ran on save (both when you're creating a new record and when you're updating it). If you want to change it, you can use +:on => :create+ to run the validation only when a new record is created or +:on => :update+ to run the validation only when a record is updated.
<ruby>
class Person < ActiveRecord::Base
- # => it will be possible to update email with a duplicated value
+ # it will be possible to update email with a duplicated value
validates_uniqueness_of :email, :on => :create
- # => it will be possible to create the record with a 'non-numerical age'
+ # it will be possible to create the record with a non-numerical age
validates_numericality_of :age, :on => :update
- # => the default (validates on both create and update)
+ # the default (validates on both create and update)
validates_presence_of :name, :on => :save
end
</ruby>
h3. Conditional Validation
-Sometimes it will make sense to validate an object just when a given predicate is satisfied. You can do that by using the +:if+ and +:unless+ options, which can take a symbol, a string or a Ruby Proc. You may use the +:if+ option when you want to specify when the validation *should* happen. If you want to specify when the validation *should not* happen, then you may use the +:unless+ option.
+Sometimes it will make sense to validate an object just when a given predicate is satisfied. You can do that by using the +:if+ and +:unless+ options, which can take a symbol, a string or a +Proc+. You may use the +:if+ option when you want to specify when the validation *should* happen. If you want to specify when the validation *should not* happen, then you may use the +:unless+ option.
-h4. Using a Symbol with :if and :unless
+h4. Using a Symbol with +:if+ and +:unless+
You can associate the +:if+ and +:unless+ options with a symbol corresponding to the name of a method that will get called right before validation happens. This is the most commonly used option.
@@ -454,9 +484,9 @@ class Order < ActiveRecord::Base
end
</ruby>
-h4. Using a String with :if and :unless
+h4. Using a String with +:if+ and +:unless+
-You can also use a string that will be evaluated using +:eval+ and needs to contain valid Ruby code. You should use this option only when the string represents a really short condition.
+You can also use a string that will be evaluated using +eval+ and needs to contain valid Ruby code. You should use this option only when the string represents a really short condition.
<ruby>
class Person < ActiveRecord::Base
@@ -464,9 +494,9 @@ class Person < ActiveRecord::Base
end
</ruby>
-h4. Using a Proc with :if and :unless
+h4. Using a Proc with +:if+ and +:unless+
-Finally, it's possible to associate +:if+ and +:unless+ with a Ruby Proc object which will be called. Using a Proc object can give you the ability to write a condition that will be executed only when the validation happens and not when your code is loaded by the Ruby interpreter. This option is best suited when writing short validation methods, usually one-liners.
+Finally, it's possible to associate +:if+ and +:unless+ with a +Proc+ object which will be called. Using a +Proc+ object gives you the ability to write an inline condition instead of a separate method. This option is best suited for one-liners.
<ruby>
class Account < ActiveRecord::Base
@@ -481,12 +511,12 @@ When the built-in validation helpers are not enough for your needs, you can writ
Simply create methods that verify the state of your models and add messages to the +errors+ collection when they are invalid. You must then register these methods by using one or more of the +validate+, +validate_on_create+ or +validate_on_update+ class methods, passing in the symbols for the validation methods' names.
-You can pass more than one symbol for each class method and the respective validations will be ran in the same order as they were registered.
+You can pass more than one symbol for each class method and the respective validations will be run in the same order as they were registered.
<ruby>
class Invoice < ActiveRecord::Base
validate :expiration_date_cannot_be_in_the_past,
- :discount_cannot_be_more_than_total_value
+ :discount_cannot_be_greater_than_total_value
def expiration_date_cannot_be_in_the_past
errors.add(:expiration_date, "can't be in the past") if
@@ -494,8 +524,8 @@ class Invoice < ActiveRecord::Base
end
def discount_cannot_be_greater_than_total_value
- errors.add(:discount, "can't be greater than total value") unless
- discount <= total_value
+ errors.add(:discount, "can't be greater than total value") if
+ discount > total_value
end
end
</ruby>
@@ -503,25 +533,18 @@ end
You can even create your own validation helpers and reuse them in several different models. Here is an example where we create a custom validation helper to validate the format of fields that represent email addresses:
<ruby>
-module ActiveRecord
- module Validations
- module ClassMethods
- def validates_email_format_of(value)
- validates_format_of value,
- :with => /\A[\w\._%-]+@[\w\.-]+\.[a-zA-Z]{2,4}\z/,
- :if => Proc.new { |u| !u.email.blank? },
- :message => "Invalid format for email address"
- end
- end
+ActiveRecord::Base.class_eval do
+ def self.validates_as_radio(attr_name, n, options={})
+ validates_inclusion_of attr_name, {:in => 1..n}.merge(options)
end
end
</ruby>
-Simply create a new validation method inside the +ActiveRecord::Validations::ClassMethods+ module. You can put this code in a file inside your application's *lib* folder, and then requiring it from your *environment.rb* or any other file inside *config/initializers*. You can use this helper like this:
+Simply reopen +ActiveRecord::Base+ and define a class method like that. You'd typically put this code somewhere in +config/initializers+. You can use this helper like this:
<ruby>
-class Person < ActiveRecord::Base
- validates_email_format_of :email_address
+class Movie < ActiveRecord::Base
+ validates_as_radio :rating, 5
end
</ruby>
@@ -529,11 +552,11 @@ h3. Working with Validation Errors
In addition to the +valid?+ and +invalid?+ methods covered earlier, Rails provides a number of methods for working with the +errors+ collection and inquiring about the validity of objects.
-The following is a list of the most commonly used methods. Please refer to the ActiveRecord::Errors documentation for an exhaustive list that covers all of the available methods.
+The following is a list of the most commonly used methods. Please refer to the +ActiveRecord::Errors+ documentation for a list of all the available methods.
-h4. errors.add_to_base
+h4. +errors.add_to_base+
-+add_to_base+ lets you add errors messages that are related to the object's state as a whole, instead of being related to a specific attribute. You can use this method when you want to say that the object is invalid, no matter the values of it's attributes. +add_to_base+ simply receives a string and uses this as the error message.
+The +add_to_base+ method lets you add errors messages that are related to the object's state as a whole, instead of being related to a specific attribute. You can use this method when you want to say that the object is invalid, no matter the values of its attributes. +add_to_base+ simply receives a string and uses this as the error message.
<ruby>
class Person < ActiveRecord::Base
@@ -543,29 +566,29 @@ class Person < ActiveRecord::Base
end
</ruby>
-h4. errors.add
+h4. +errors.add+
-+add+ lets you manually add messages that are related to particular attributes. Note that Rails will prepend the name of the attribute to the error message you pass it. You can use the +full_messages+ method to view the messages in the form they might be displayed to a user. +add+ receives a symbol with the name of the attribute that you want to add the message to, and the message itself.
+The +add+ method lets you manually add messages that are related to particular attributes. You can use the +full_messages+ method to view the messages in the form they might be displayed to a user. Those particular messages get the attribute name prepended (and capitalized). +add+ receives the name of the attribute you want to add the message to, and the message itself.
<ruby>
class Person < ActiveRecord::Base
def a_method_used_for_validation_purposes
- errors.add(:name, "cannot contain the characters !@#$%*()_-+=")
+ errors.add(:name, "cannot contain the characters !@#%*()_-+=")
end
end
-person = Person.create(:name => "!@#$")
+person = Person.create(:name => "!@#")
person.errors.on(:name)
-# => "is too short (minimum is 3 characters)"
+ # => "cannot contain the characters !@#%*()_-+="
person.errors.full_messages
-# => ["Name is too short (minimum is 3 characters)"]
+ # => ["Name cannot contain the characters !@#%*()_-+="]
</ruby>
-h4. errors.on
+h4. +errors.on+
-+on+ is used when you want to check the error messages for a specific attribute. It will return different kinds of objects depending on the state of the +errors+ collection for the given attribute. If there are no errors related to the attribute, +on+ will return +nil+. If there is just one errors message for this attribute, +on+ will return a string with the message. When +errors+ holds two or more error messages for the attribute, +on+ will return an array of strings, each one with one error message.
+The +on+ method is used when you want to check the error messages for a specific attribute. It returns different kinds of objects depending on the state of the +errors+ collection for the given attribute. If there are no errors related to the attribute +on+ returns +nil+. If there is just one error message for this attribute +on+ returns a string with the message. When +errors+ holds two or more error messages for the attribute, +on+ returns an array of strings, each one with one error message.
<ruby>
class Person < ActiveRecord::Base
@@ -580,17 +603,17 @@ person.errors.on(:name) # => nil
person = Person.new(:name => "JD")
person.valid? # => false
person.errors.on(:name)
-# => "is too short (minimum is 3 characters)"
+ # => "is too short (minimum is 3 characters)"
person = Person.new
person.valid? # => false
person.errors.on(:name)
-# => ["can't be blank", "is too short (minimum is 3 characters)"]
+ # => ["can't be blank", "is too short (minimum is 3 characters)"]
</ruby>
-h4. errors.clear
+h4. +errors.clear+
-+clear+ is used when you intentionally want to clear all the messages in the +errors+ collection. Of course, calling +errors.clear+ upon an invalid object won't actually make it valid: the +errors+ collection will now be empty, but the next time you call +valid?+ or any method that tries to save this object to the database, the validations will run again. If any of the validations fail, the +errors+ collection will be filled again.
+The +clear+ method is used when you intentionally want to clear all the messages in the +errors+ collection. Of course, calling +errors.clear+ upon an invalid object won't actually make it valid: the +errors+ collection will now be empty, but the next time you call +valid?+ or any method that tries to save this object to the database, the validations will run again. If any of the validations fail, the +errors+ collection will be filled again.
<ruby>
class Person < ActiveRecord::Base
@@ -601,7 +624,7 @@ end
person = Person.new
person.valid? # => false
person.errors.on(:name)
-# => ["can't be blank", "is too short (minimum is 3 characters)"]
+ # => ["can't be blank", "is too short (minimum is 3 characters)"]
person.errors.clear
person.errors.empty? # => true
@@ -609,31 +632,36 @@ person.errors.empty? # => true
p.save # => false
p.errors.on(:name)
-# => ["can't be blank", "is too short (minimum is 3 characters)"]
+ # => ["can't be blank", "is too short (minimum is 3 characters)"]
</ruby>
-h4. errors.size
+h4. +errors.size+
-+size+ returns the total number of errors added. Two errors added to the same object will be counted as such.
+The +size+ method returns the total number of error messages for the object.
<ruby>
class Person < ActiveRecord::Base
validates_presence_of :name
- validates_length_of :name, :minimum => 3
+ validates_length_of :name, :minimum => 3
+ validates_presence_of :email
end
person = Person.new
person.valid? # => false
-person.errors.size # => 2
+person.errors.size # => 3
+
+person = Person.new(:name => "Andrea", :email => "andrea@example.com")
+person.valid? # => true
+person.errors.size # => 0
</ruby>
h3. Displaying Validation Errors in the View
Rails provides built-in helpers to display the error messages of your models in your view templates.
-h4. error_messages and error_messages_for
+h4. +error_messages+ and +error_messages_for+
-When creating a form with the form_for helper, you can use the error_messages method on the form builder to render all failed validation messages for the current model instance.
+When creating a form with the +form_for+ helper, you can use the +error_messages+ method on the form builder to render all failed validation messages for the current model instance.
<ruby>
class Product < ActiveRecord::Base
@@ -659,6 +687,8 @@ end
<% end %>
</erb>
+To get the idea, if you submit the form with empty fields you typically get this back, though styles are indeed missing by default:
+
!images/error_messages.png(Error messages)!
You can also use the +error_messages_for+ helper to display the error messages of a model assigned to a view template. It's very similar to the previous example and will achieve exactly the same result.
@@ -685,53 +715,52 @@ If you pass +nil+ to any of these options, it will get rid of the respective sec
h4. Customizing the Error Messages CSS
-It's also possible to change the CSS classes used by the +error_messages+ helper. These classes are automatically defined at the *scaffold.css* file, generated by the scaffold script. If you're not using scaffolding, you can still define those CSS classes at your CSS files. Here is a list of the default CSS classes.
+The selectors to customize the style of error messages are:
-* +.fieldWithErrors+ - Style for the form fields with errors.
+* +.fieldWithErrors+ - Style for the form fields and labels with errors.
* +#errorExplanation+ - Style for the +div+ element with the error messages.
* +#errorExplanation h2+ - Style for the header of the +div+ element.
* +#errorExplanation p+ - Style for the paragraph that holds the message that appears right below the header of the +div+ element.
-* +#errorExplanation ul li+ - Style for the list of error messages.
+* +#errorExplanation ul li+ - Style for the list items with individual error messages.
+
+Scaffolding for example generates +public/stylesheets/scaffold.css+, which defines the red-based style you saw above.
+
+The name of the class and the id can be changed with the +:class+ and +:id+ options, accepted by both helpers.
h4. Customizing the Error Messages HTML
-By default, form fields with errors are displayed enclosed by a +div+ element with the +fieldWithErrors+ CSS class. However, it's possible to override the way Rails treats those fields by default.
+By default, form fields with errors are displayed enclosed by a +div+ element with the +fieldWithErrors+ CSS class. However, it's possible to override that.
+
+The way form fields with errors are treated is defined by +ActionView::Base.field_error_proc+. This is a +Proc+ that receives two parameters:
+
+* A string with the HTML tag
+* An instance of +ActionView::Helpers::InstanceTag+.
Here is a simple example where we change the Rails behaviour to always display the error messages in front of each of the form fields with errors. The error messages will be enclosed by a +span+ element with a +validation-error+ CSS class. There will be no +div+ element enclosing the +input+ element, so we get rid of that red border around the text field. You can use the +validation-error+ CSS class to style it anyway you want.
<ruby>
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
if instance.error_message.kind_of?(Array)
- %(#{html_tag}<span class='validation-error'>&nbsp;
+ %(#{html_tag}<span class="validation-error">&nbsp;
#{instance.error_message.join(',')}</span>)
else
- %(#{html_tag}<span class='validation-error'>&nbsp;
+ %(#{html_tag}<span class="validation-error">&nbsp;
#{instance.error_message}</span>)
end
end
</ruby>
-This will result in something like the following content:
+This will result in something like the following:
!images/validation_error_messages.png(Validation error messages)!
-The way form fields with errors are treated is defined by the +ActionView::Base.field_error_proc+ Ruby Proc. This Proc receives two parameters:
-
-* A string with the HTML tag
-* An object of the +ActionView::Helpers::InstanceTag+ class.
-
h3. Callbacks Overview
-Callbacks are methods that get called at certain moments of an object's lifecycle. With callbacks it's possible to write code that will run whenever an Active Record object is created, saved, updated, deleted or loaded from the database.
-
-# TODO discuss what does/doesn't trigger callbacks, like we did in the validations section (e.g. destroy versus delete).
-# Consider moving up to the (new) intro overview section, before getting into details.
-# http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002220
-# http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
+Callbacks are methods that get called at certain moments of an object's lifecycle. With callbacks it's possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, or loaded from the database.
h4. Callback Registration
-In order to use the available callbacks, you need to register them. You can do that by implementing them as an ordinary methods, and then using a macro-style class method to register then as callbacks.
+In order to use the available callbacks, you need to register them. You can do that by implementing them as ordinary methods, and then using a macro-style class method to register them as callbacks.
<ruby>
class User < ActiveRecord::Base
@@ -741,7 +770,7 @@ class User < ActiveRecord::Base
protected
def ensure_login_has_a_value
- if self.login.nil?
+ if login.nil?
self.login = email unless email.blank?
end
end
@@ -754,19 +783,166 @@ The macro-style class methods can also receive a block. Consider using this styl
class User < ActiveRecord::Base
validates_presence_of :login, :email
- before_create {|user| user.name = user.login.capitalize if user.name.blank?}
+ before_create {|user| user.name = user.login.capitalize
+ if user.name.blank?}
end
</ruby>
It's considered good practice to declare callback methods as being protected or private. If left public, they can be called from outside of the model and violate the principle of object encapsulation.
+h3. Available Callbacks
+
+Here is a list with all the available Active Record callbacks, listed in the same order in which they will get called during the respective operations:
+
+h4. Creating an Object
+
+* +before_validation+
+* +before_validation_on_create+
+* +after_validation+
+* +after_validation_on_create+
+* +before_save+
+* +before_create+
+* INSERT OPERATION
+* +after_create+
+* +after_save+
+
+h4. Updating an Object
+
+* +before_validation+
+* +before_validation_on_update+
+* +after_validation+
+* +after_validation_on_update+
+* +before_save+
+* +before_update+
+* UPDATE OPERATION
+* +after_update+
+* +after_save+
+
+h4. Destroying an Object
+
+* +before_destroy+
+* DELETE OPERATION
+* +after_destroy+
+
+WARNING. +after_save+ runs both on create and update, but always _after_ the more specific callbacks +after_create+ and +after_update+, no matter the order in which the macro calls were executed.
+
+h4. +after_initialize+ and +after_find+
+
+The +after_initialize+ callback will be called whenever an Active Record object is instantiated, either by directly using +new+ or when a record is loaded from the database. It can be useful to avoid the need to directly override your Active Record +initialize+ method.
+
+The +after_find+ callback will be called whenever Active Record loads a record from the database. +after_find+ is called before +after_initialize+ if both are defined.
+
+The +after_initialize+ and +after_find+ callbacks are a bit different from the others. They have no +before_*+ counterparts, and the only way to register them is by defining them as regular methods. If you try to register +after_initialize+ or +after_find+ using macro-style class methods, they will just be ignored. This behaviour is due to performance reasons, since +after_initialize+ and +after_find+ will both be called for each record found in the database, significantly slowing down the queries.
+
+<ruby>
+class User < ActiveRecord::Base
+ def after_initialize
+ puts "You have initialized an object!"
+ end
+
+ def after_find
+ puts "You have found an object!"
+ end
+end
+
+>> User.new
+You have initialized an object!
+=> #<User id: nil>
+
+>> User.first
+You have found an object!
+You have initialized an object!
+=> #<User id: 1>
+</ruby>
+
+h3. Running Callbacks
+
+The following methods trigger callbacks:
+
+* +create+
+* +create!+
+* +decrement!+
+* +destroy+
+* +destroy_all+
+* +increment!+
+* +save+
+* +save!+
+* +save(false)+
+* +toggle!+
+* +update+
+* +update_attribute+
+* +update_attributes+
+* +update_attributes!+
+* +valid?+
+
+Additionally, the +after_find+ callback is triggered by the following finder methods:
+
+* +all+
+* +first+
+* +find+
+* +find_all_by_<em>attribute</em>+
+* +find_by_<em>attribute</em>+
+* +find_by_<em>attribute</em>!+
+* +last+
+
+The +after_initialize+ callback is triggered every time a new object of the class is initialized.
+
+h3. Skipping Callbacks
+
+Just as with validations, it's also possible to skip callbacks. These methods should be used with caution, however, because important business rules and application logic may be kept in callbacks. Bypassing them without understanding the potential implications may lead to invalid data.
+
+* +decrement+
+* +decrement_counter+
+* +delete+
+* +delete_all+
+* +find_by_sql+
+* +increment+
+* +increment_counter+
+* +toggle+
+* +update_all+
+* +update_counters+
+
+h3. Halting Execution
+
+As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks, and the database operation to be executed.
+
+The whole callback chain is wrapped in a transaction. If any before callback method returns exactly +false+ or raises an exception the execution chain gets halted and a ROLLBACK is issued. After callbacks can only accomplish that by raising an exception.
+
+WARNING. Raising an arbitrary exception may break code that expects +save+ and friends not to fail like that. The +ActiveRecord::Rollback+ exception is thought precisely to tell Active Record a rollback is going on. That one is internally captured but not reraised.
+
+h3. Relational Callbacks
+
+Callbacks work through model relationships, and can even be defined by them. Let's take an example where a user has many posts. In our example, a user's posts should be destroyed if the user is destroyed. So, we'll add an +after_destroy+ callback to the +User+ model by way of its relationship to the +Post+ model.
+
+<ruby>
+class User < ActiveRecord::Base
+ has_many :posts, :dependent => :destroy
+end
+
+class Post < ActiveRecord::Base
+ after_destroy :log_destroy_action
+
+ def log_destroy_action
+ puts 'Post destroyed'
+ end
+end
+
+>> user = User.first
+=> #<User id: 1>
+>> user.posts.create!
+=> #<Post id: 1, user_id: 1>
+>> user.destroy
+Post destroyed
+=> #<User id: 1>
+</ruby>
+
h3. Conditional Callbacks
-Like in validations, we can also make our callbacks conditional, calling then only when a given predicate is satisfied. You can do that by using the +:if+ and +:unless+ options, which can take a symbol, a string or a Ruby Proc. You may use the +:if+ option when you want to specify when the callback *should* get called. If you want to specify when the callback *should not* be called, then you may use the +:unless+ option.
+Like in validations, we can also make our callbacks conditional, calling them only when a given predicate is satisfied. You can do that by using the +:if+ and +:unless+ options, which can take a symbol, a string or a +Proc+. You may use the +:if+ option when you want to specify when the callback *should* get called. If you want to specify when the callback *should not* be called, then you may use the +:unless+ option.
-h4. Using :if and :unless with a Symbol
+h4. Using +:if+ and +:unless+ with a Symbol
-You can associate the +:if+ and +:unless+ options with a symbol corresponding to the name of a method that will get called right before the callback. If this method returns +false+ the callback won't be executed. This is the most common option. Using this form of registration it's also possible to register several different methods that should be called to check the if the callback should be executed.
+You can associate the +:if+ and +:unless+ options with a symbol corresponding to the name of a method that will get called right before the callback. If this method returns +false+ the callback won't be executed. This is the most common option. Using this form of registration it's also possible to register several different methods that should be called to check if the callback should be executed.
<ruby>
class Order < ActiveRecord::Base
@@ -774,9 +950,9 @@ class Order < ActiveRecord::Base
end
</ruby>
-h4. Using :if and :unless with a String
+h4. Using +:if+ and +:unless+ with a String
-You can also use a string that will be evaluated using +:eval+ and needs to contain valid Ruby code. You should use this option only when the string represents a really short condition.
+You can also use a string that will be evaluated using +eval+ and needs to contain valid Ruby code. You should use this option only when the string represents a really short condition.
<ruby>
class Order < ActiveRecord::Base
@@ -784,9 +960,9 @@ class Order < ActiveRecord::Base
end
</ruby>
-h4. Using :if and :unless with a Proc
+h4. Using +:if+ and +:unless+ with a Proc
-Finally, it's possible to associate +:if+ and +:unless+ with a Ruby Proc object. This option is best suited when writing short validation methods, usually one-liners.
+Finally, it's possible to associate +:if+ and +:unless+ with a +Proc+ object. This option is best suited when writing short validation methods, usually one-liners.
<ruby>
class Order < ActiveRecord::Base
@@ -806,64 +982,17 @@ class Comment < ActiveRecord::Base
end
</ruby>
-h3. Available Callbacks
-
-Here is a list with all the available Active Record callbacks, listed in the same order in which they will get called during the respective operations.
-
-h4. Creating and/or Updating an Object
-
-* +before_validation+
-* +after_validation+
-* +before_save+
-* INSERT OR UPDATE OPERATION
-* +after_save+
-
-h4. Creating an Object
-
-* +before_validation_on_create+
-* +after_validation_on_create+
-* +before_create+
-* INSERT OPERATION
-* +after_create+
-
-h4. Updating an Object
-
-* +before_validation_on_update+
-* +after_validation_on_update+
-* +before_update+
-* UPDATE OPERATION
-* +after_update+
-
-h4. Destroying an Object
-
-* +before_destroy+
-* DELETE OPERATION
-* +after_destroy+
-
-CAUTION: The +before_destroy+ and +after_destroy+ callbacks will only be called if you delete the model using either the +destroy+ instance method or one of the +destroy+ or +destroy_all+ class methods of your Active Record class. If you use +delete+ or +delete_all+ no callback operations will run, since Active Record will not instantiate any objects, accessing the records to be deleted directly in the database.
-
-h4. after_initialize and after_find
-
-The +after_initialize+ callback will be called whenever an Active Record object is instantiated, either by directly using +new+ or when a record is loaded from the database. It can be useful to avoid the need to directly override your Active Record +initialize+ method.
-
-The +after_find+ callback will be called whenever Active Record loads a record from the database. When used together with +after_initialize+ it will run first, since Active Record will first read the record from the database and them create the model object that will hold it.
-
-The +after_initialize+ and +after_find+ callbacks are a bit different from the others, since the only way to register those callbacks is by defining them as methods. If you try to register +after_initialize+ or +after_find+ using macro-style class methods, they will just be ignored. This behaviour is due to performance reasons, since +after_initialize+ and +after_find+ will both be called for each record found in the database, significantly slowing down the queries.
-
-h3. Halting Execution
-
-As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks and the database operation to be executed. However, if at any moment one of the +before_create+, +before_save+, +before_update+ or +before_destroy+ callback methods returns a boolean +false+ (not +nil+) value or raise and exception, this execution chain will be halted and the desired operation will not complete: your model will not get persisted in the database, or your records will not get deleted and so on. It's because the whole callback chain is wrapped in a transaction, so raising an exception or returning +false+ fires a database ROLLBACK.
-
h3. Callback Classes
-Sometimes the callback methods that you'll write will be useful enough to be reused at other models. Active Record makes it possible to create classes that encapsulate the callback methods, so it becomes very easy to reuse them.
+Sometimes the callback methods that you'll write will be useful enough to be reused by other models. Active Record makes it possible to create classes that encapsulate the callback methods, so it becomes very easy to reuse them.
-Here's an example where we create a class with a after_destroy callback for a PictureFile model.
+Here's an example where we create a class with an +after_destroy+ callback for a +PictureFile+ model.
<ruby>
class PictureFileCallbacks
def after_destroy(picture_file)
- File.delete(picture_file.filepath) if File.exists?(picture_file.filepath)
+ File.delete(picture_file.filepath)
+ if File.exists?(picture_file.filepath)
end
end
</ruby>
@@ -876,17 +1005,18 @@ class PictureFile < ActiveRecord::Base
end
</ruby>
-Note that we needed to instantiate a new PictureFileCallbacks object, since we declared our callback as an instance method. Sometimes it will make more sense to have it as a class method.
+Note that we needed to instantiate a new +PictureFileCallbacks+ object, since we declared our callback as an instance method. Sometimes it will make more sense to have it as a class method.
<ruby>
class PictureFileCallbacks
def self.after_destroy(picture_file)
- File.delete(picture_file.filepath) if File.exists?(picture_file.filepath)
+ File.delete(picture_file.filepath)
+ if File.exists?(picture_file.filepath)
end
end
</ruby>
-If the callback method is declared this way, it won't be necessary to instantiate a PictureFileCallbacks object.
+If the callback method is declared this way, it won't be necessary to instantiate a +PictureFileCallbacks+ object.
<ruby>
class PictureFile < ActiveRecord::Base
@@ -898,9 +1028,9 @@ You can declare as many callbacks as you want inside your callback classes.
h3. Observers
-Observers are similar to callbacks, but with important differences. Whereas callbacks can pollute a model with code that isn't directly related to its purpose, observers allow you to add functionality outside of a model. For example, it could be argued that a +User+ model should not include code to send registration confirmation emails. Whenever you use callbacks with code that isn't directly related to your model, you may want to consider creating an observer instead.
+Observers are similar to callbacks, but with important differences. Whereas callbacks can pollute a model with code that isn't directly related to its purpose, observers allow you to add the same functionality outside of a model. For example, it could be argued that a +User+ model should not include code to send registration confirmation emails. Whenever you use callbacks with code that isn't directly related to your model, you may want to consider creating an observer instead.
-h4. Creating observers
+h4. Creating Observers
For example, imagine a +User+ model where we want to send an email every time a new user is created. Because sending emails is not directly related to our model's purpose, we could create an observer to contain this functionality.
@@ -916,18 +1046,18 @@ As with callback classes, the observer's methods receive the observed model as a
h4. Registering Observers
-Observers should be placed inside of your *app/models* directory and registered in your application's *config/environment.rb* file. For example, the +UserObserver+ above would be saved as *app/models/user_observer.rb* and registered in *config/environment.rb*.
+Observers are conventionally placed inside of your +app/models+ directory and registered in your application's +config/environment.rb+ file. For example, the +UserObserver+ above would be saved as +app/models/user_observer.rb+ and registered in +config/environment.rb+ this way:
<ruby>
# Activate observers that should always be running
config.active_record.observers = :user_observer
</ruby>
-As usual, settings in *config/environments/* take precedence over those in *config/environment.rb*. So, if you prefer that an observer not run in all environments, you can simply register it in a specific environment instead.
+As usual, settings in +config/environments+ take precedence over those in +config/environment.rb+. So, if you prefer that an observer not run in all environments, you can simply register it in a specific environment instead.
h4. Sharing Observers
-By default, Rails will simply strip 'observer' from an observer's name to find the model it should observe. However, observers can also be used to add behaviour to more than one model, and so it's possible to manually specify the models that our observer should observe.
+By default, Rails will simply strip "Observer" from an observer's name to find the model it should observe. However, observers can also be used to add behaviour to more than one model, and so it's possible to manually specify the models that our observer should observe.
<ruby>
class MailerObserver < ActiveRecord::Observer
@@ -939,7 +1069,7 @@ class MailerObserver < ActiveRecord::Observer
end
</ruby>
-In this example, the +after_create+ method would be called whenever a +Registration+ or +User+ was created. Note that this new +MailerObserver+ would also need to be registered in *config/environment.rb* in order to take effect.
+In this example, the +after_create+ method would be called whenever a +Registration+ or +User+ was created. Note that this new +MailerObserver+ would also need to be registered in +config/environment.rb+ in order to take effect.
<ruby>
# Activate observers that should always be running
@@ -950,6 +1080,7 @@ h3. Changelog
"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213/tickets/26-active-record-validations-and-callbacks
+* March 7, 2009: Callbacks revision by Trevor Turk
* February 10, 2009: Observers revision by Trevor Turk
* February 5, 2009: Initial revision by Trevor Turk
* January 9, 2009: Initial version by "Cássio Marques":credits.html#cmarques
diff --git a/railties/guides/source/association_basics.textile b/railties/guides/source/association_basics.textile
index 3c03c825cd..03e22bd6fe 100644
--- a/railties/guides/source/association_basics.textile
+++ b/railties/guides/source/association_basics.textile
@@ -76,7 +76,7 @@ In Rails, an _association_ is a connection between two Active Record models. Ass
In the remainder of this guide, you'll learn how to declare and use the various forms of associations. But first, a quick introduction to the situations where each association type is appropriate.
-h4. The +belongs_to+ association
+h4. The +belongs_to+ Association
A +belongs_to+ association sets up a one-to-one connection with another model, such that each instance of the declaring model "belongs to" one instance of the other model. For example, if your application includes customers and orders, and each order can be assigned to exactly one customer, you'd declare the order model this way:
@@ -88,7 +88,7 @@ end
!images/belongs_to.png(belongs_to Association Diagram)!
-h4. The +has_one+ association
+h4. The +has_one+ Association
A +has_one+ association also sets up a one-to-one connection with another model, but with somewhat different semantics (and consequences). This association indicates that each instance of a model contains or possesses one instance of another model. For example, if each supplier in your application has only one account, you'd declare the supplier model like this:
@@ -100,7 +100,7 @@ end
!images/has_one.png(has_one Association Diagram)!
-h4. The +has_many+ association
+h4. The +has_many+ Association
A +has_many+ association indicates a one-to-many connection with another model. You'll often find this association on the "other side" of a +belongs_to+ association. This association indicates that each instance of the model has zero or more instances of another model. For example, in an application containing customers and orders, the customer model could be declared like this:
@@ -114,7 +114,7 @@ NOTE: The name of the other model is pluralized when declaring a +has_many+ asso
!images/has_many.png(has_many Association Diagram)!
-h4. The +has_many :through+ association
+h4. The +has_many :through+ Association
A +has_many :through+ association is often used to set up a many-to-many connection with another model. This association indicates that the declaring model can be matched with zero or more instances of another model by proceeding _through_ a third model. For example, consider a medical practice where patients make appointments to see physicians. The relevant association declarations could look like this:
@@ -155,7 +155,7 @@ class Paragraph < ActiveRecord::Base
end
</ruby>
-h4. The +has_one :through+ association
+h4. The +has_one :through+ Association
A +has_one :through+ association sets up a one-to-one connection with another model. This association indicates that the declaring model can be matched with one instance of another model by proceeding _through_ a third model. For example, if each supplier has one account, and each account is associated with one account history, then the customer model could look like this:
@@ -177,7 +177,7 @@ end
!images/has_one_through.png(has_one :through Association Diagram)!
-h4. The +has_and_belongs_to_many+ association
+h4. The +has_and_belongs_to_many+ Association
A +has_and_belongs_to_many+ association creates a direct many-to-many connection with another model, with no intervening model. For example, if your application includes assemblies and parts, with each assembly having many parts and each part appearing in many assemblies, you could declare the models this way:
@@ -193,7 +193,7 @@ end
!images/habtm.png(has_and_belongs_to_many Association Diagram)!
-h4. Choosing between +belongs_to+ and +has_one+
+h4. Choosing Between +belongs_to+ and +has_one+
If you want to set up a 1–1 relationship between two models, you'll need to add +belongs_to+ to one, and +has_one+ to the other. How do you know which is which?
@@ -235,7 +235,7 @@ end
NOTE: Using +t.integer :supplier_id+ makes the foreign key naming obvious and explicit. In current versions of Rails, you can abstract away this implementation detail by using +t.references :supplier+ instead.
-h4. Choosing between +has_many :through+ and +has_and_belongs_to_many+
+h4. Choosing Between +has_many :through+ and +has_and_belongs_to_many+
Rails offers two different ways to declare a many-to-many relationship between models. The simpler way is to use +has_and_belongs_to_many+, which allows you to make the association directly:
@@ -272,7 +272,7 @@ The simplest rule of thumb is that you should set up a +has_many :through+ relat
You should use +has_many :through+ if you need validations, callbacks, or extra attributes on the join model.
-h4. Polymorphic associations
+h4. Polymorphic Associations
A slightly more advanced twist on associations is the _polymorphic association_. With polymorphic associations, a model can belong to more than one other model, on a single association. For example, you might have a picture model that belongs to either an employee model or a product model. Here's how this could be declared:
@@ -333,7 +333,7 @@ end
!images/polymorphic.png(Polymorphic Association Diagram)!
-h4. Self joins
+h4. Self Joins
In designing a data model, you will sometimes find a model that should have a relation to itself. For example, you may want to store all employees in a single database model, but be able to trace relationships such as between manager and subordinates. This situation can be modeled with self-joining associations:
@@ -356,7 +356,7 @@ Here are a few things you should know to make efficient use of Active Record ass
* Updating the schema
* Controlling association scope
-h4. Controlling caching
+h4. Controlling Caching
All of the association methods are built around caching, which keeps the result of the most recent query available for further operations. The cache is even shared across methods. For example:
@@ -375,15 +375,15 @@ customer.orders(true).empty? # discards the cached copy of orders
# and goes back to the database
</ruby>
-h4. Avoiding name collisions
+h4. Avoiding Name Collisions
You are not free to use just any name for your associations. Because creating an association adds a method with that name to the model, it is a bad idea to give an association a name that is already used for an instance method of +ActiveRecord::Base+. The association method would override the base method and break things. For instance, +attributes+ or +connection+ are bad names for associations.
-h4. Updating the schema
+h4. Updating the Schema
Associations are extremely useful, but they are not magic. You are responsible for maintaining your database schema to match your associations. In practice, this means two things, depending on what sort of associations you are creating. For +belongs_to+ associations you need to create foreign keys, and for +has_and_belongs_to_many+ associations you need to create the appropriate join table.
-h5. Creating foreign Keys for +belongs_to+ associations
+h5. Creating Foreign Keys for +belongs_to+ Associations
When you declare a +belongs_to+ association, you need to create foreign keys as appropriate. For example, consider this model:
@@ -413,7 +413,7 @@ end
If you create an association some time after you build the underlying model, you need to remember to create an +add_column+ migration to provide the necessary foreign key.
-h5. Creating join tables for +has_and_belongs_to_many+ associations
+h5. Creating Join Tables for +has_and_belongs_to_many+ Associations
If you create a +has_and_belongs_to_many+ association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the +:join_table+ option, Active Record creates the name by using the lexical order of the class names. So a join between customer and order models will give the default join table name of "customers_orders" because "c" outranks "o" in lexical ordering.
@@ -450,7 +450,7 @@ end
We pass +:id => false+ to +create_table+ because that table does not represent a model. That's required for the association to work properly. If you observe any strange behaviour in a +has_and_belongs_to_many+ association like mangled models IDs, or exceptions about conflicting IDs chances are you forgot that bit.
-h4. Controlling association scope
+h4. Controlling Association Scope
By default, associations look for objects only within the current module's scope. This can be important when you declare Active Record models within a module. For example:
@@ -510,11 +510,11 @@ h3. Detailed Association Reference
The following sections give the details of each type of association, including the methods that they add and the options that you can use when declaring an association.
-h4. +belongs_to+ association reference
+h4. +belongs_to+ Association Reference
The +belongs_to+ association creates a one-to-one match with another model. In database terms, this association says that this class contains the foreign key. If the other class contains the foreign key, then you should use +has_one+ instead.
-h5. Methods added by +belongs_to+
+h5. Methods Added by +belongs_to+
When you declare a +belongs_to+ association, the declaring class automatically gains four methods related to the association:
@@ -724,7 +724,7 @@ NOTE: There's no need to use +:include+ for immediate associations - that is, if
h6. +:polymorphic+
-Passing +true+ to the +:polymorphic+ option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail <a href="#polymorphicassociations">earlier in this guide</a>.
+Passing +true+ to the +:polymorphic+ option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail <a href="#polymorphic-associations">earlier in this guide</a>.
h6. +:readonly+
@@ -740,7 +740,7 @@ h6. +:validate+
If you set the +:validate+ option to +true+, then associated objects will be validated whenever you save this object. By default, this is +false+: associated objects will not be validated when this object is saved.
-h5. How to know whether there's an associated object?
+h5. How To Know Whether There's an Associated Object?
To know whether there's and associated object just check <tt><em>association</em>.nil?</tt>:
@@ -750,15 +750,15 @@ if @order.customer.nil?
end
</ruby>
-h5. When are objects saved?
+h5. When are Objects Saved?
Assigning an object to a +belongs_to+ association does _not_ automatically save the object. It does not save the associated object either.
-h4. +has_one+ association reference
+h4. +has_one+ Association Reference
The +has_one+ association creates a one-to-one match with another model. In database terms, this association says that the other class contains the foreign key. If this class contains the foreign key, then you should use +belongs_to+ instead.
-h5. Methods added by +has_one+
+h5. Methods Added by +has_one+
When you declare a +has_one+ association, the declaring class automatically gains four methods related to the association:
@@ -848,7 +848,7 @@ The +has_one+ association supports these options:
h6. +:as+
-Setting the +:as+ option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail <a href="#polymorphicassociations">earlier in this guide</a>.
+Setting the +:as+ option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail <a href="#polymorphic-associations">earlier in this guide</a>.
h6. +:autosave+
@@ -952,13 +952,13 @@ The +:source_type+ option specifies the source association type for a +has_one :
h6. :through
-The +:through+ option specifies a join model through which to perform the query. +has_one :through+ associations were discussed in detail <a href="#thehas-onethroughassociation">earlier in this guide</a>.
+The +:through+ option specifies a join model through which to perform the query. +has_one :through+ associations were discussed in detail <a href="#the-has-one-through-association">earlier in this guide</a>.
h6. +:validate+
If you set the +:validate+ option to +true+, then associated objects will be validated whenever you save this object. By default, this is +false+: associated objects will not be validated when this object is saved.
-h5. How to know whether there's an associated object?
+h5. How To Know Whether There's an Associated Object?
To know whether there's and associated object just check <tt><em>association</em>.nil?</tt>:
@@ -968,7 +968,7 @@ if @supplier.account.nil?
end
</ruby>
-h5. When are objects saved?
+h5. When are Objects Saved?
When you assign an object to a +has_one+ association, that object is automatically saved (in order to update its foreign key). In addition, any object being replaced is also automatically saved, because its foreign key will change too.
@@ -978,11 +978,11 @@ If the parent object (the one declaring the +has_one+ association) is unsaved (t
If you want to assign an object to a +has_one+ association without saving the object, use the <tt><em>association</em>.build</tt> method.
-h4. +has_many+ association reference
+h4. +has_many+ Association Reference
The +has_many+ association creates a one-to-many relationship with another model. In database terms, this association says that the other class will have a foreign key that refers to instances of this class.
-h5. Methods added
+h5. Methods Added
When you declare a +has_many+ association, the declaring class automatically gains 13 methods related to the association:
@@ -1158,7 +1158,7 @@ The +has_many+ association supports these options:
h6. +:as+
-Setting the +:as+ option indicates that this is a polymorphic association, as discussed <a href="#polymorphicassociations">earlier in this guide</a>.
+Setting the +:as+ option indicates that this is a polymorphic association, as discussed <a href="#polymorphic-associations">earlier in this guide</a>.
h6. +:autosave+
@@ -1210,7 +1210,7 @@ NOTE: This option is ignored when you use the +:through+ option on the associati
h6. +:extend+
-The +:extend+ option specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#associationextensions">later in this guide</a>.
+The +:extend+ option specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>.
h6. +:finder_sql+
@@ -1323,7 +1323,7 @@ The +:source_type+ option specifies the source association type for a +has_many
h6. +:through+
-The +:through+ option specifies a join model through which to perform the query. +has_many :through+ associations provide a way to implement many-to-many relationships, as discussed <a href="#thehas-manythroughassociation">earlier in this guide</a>.
+The +:through+ option specifies a join model through which to perform the query. +has_many :through+ associations provide a way to implement many-to-many relationships, as discussed <a href="#the-has-many-through-association">earlier in this guide</a>.
h6. +:uniq+
@@ -1333,7 +1333,7 @@ h6. +:validate+
If you set the +:validate+ option to +false+, then associated objects will not be validated whenever you save this object. By default, this is +true+: associated objects will be validated when this object is saved.
-h5. When are objects saved?
+h5. When are Objects Saved?
When you assign an object to a +has_many+ association, that object is automatically saved (in order to update its foreign key). If you assign multiple objects in one statement, then they are all saved.
@@ -1343,11 +1343,11 @@ If the parent object (the one declaring the +has_many+ association) is unsaved (
If you want to assign an object to a +has_many+ association without saving the object, use the <tt><em>collection</em>.build</tt> method.
-h4. +has_and_belongs_to_many+ association reference
+h4. +has_and_belongs_to_many+ Association Reference
The +has_and_belongs_to_many+ association creates a many-to-many relationship with another model. In database terms, this associates two classes via an intermediate join table that includes foreign keys referring to each of the classes.
-h5. Methods added
+h5. Methods Added
When you declare a +has_and_belongs_to_many+ association, the declaring class automatically gains 13 methods related to the association:
@@ -1391,7 +1391,7 @@ assemblies.build(attributes = {}, ...)
assemblies.create(attributes = {})
</ruby>
-h6. Additional column methods
+h6. Additional Column Methods
If the join table for a +has_and_belongs_to_many+ association has additional columns beyond the two foreign keys, these columns will be added as attributes to records retrieved via that association. Records returned with additional attributes will always be read-only, because Rails cannot save changes to those attributes.
@@ -1589,7 +1589,7 @@ Normally Rails automatically generates the proper SQL to remove links between th
h6. +:extend+
-The +:extend+ option specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#associationextensions">later in this guide</a>.
+The +:extend+ option specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>.
h6. +:finder_sql+
@@ -1670,7 +1670,7 @@ h6. +:validate+
If you set the +:validate+ option to +false+, then associated objects will not be validated whenever you save this object. By default, this is +true+: associated objects will be validated when this object is saved.
-h5. When are objects saved?
+h5. When are Objects Saved?
When you assign an object to a +has_and_belongs_to_many+ association, that object is automatically saved (in order to update the join table). If you assign multiple objects in one statement, then they are all saved.
@@ -1680,7 +1680,7 @@ If the parent object (the one declaring the +has_and_belongs_to_many+ associatio
If you want to assign an object to a +has_and_belongs_to_many+ association without saving the object, use the <tt><em>collection</em>.build</tt> method.
-h4. Association callbacks
+h4. Association Callbacks
Normal callbacks hook into the lifecycle of Active Record objects, allowing you to work with those objects at various points. For example, you can use a +:before_save+ callback to cause something to happen just before an object is saved.
@@ -1724,7 +1724,7 @@ end
If a +before_add+ callback throws an exception, the object does not get added to the collection. Similarly, if a +before_remove+ callback throws an exception, the object does not get removed from the collection.
-h4. Association extensions
+h4. Association Extensions
You're not limited to the functionality that Rails automatically builds into association proxy objects. You can also extend these objects through anonymous modules, adding new finders, creators, or other methods. For example:
diff --git a/railties/guides/source/caching_with_rails.textile b/railties/guides/source/caching_with_rails.textile
index b1c1af8be4..dd1b76bbc4 100644
--- a/railties/guides/source/caching_with_rails.textile
+++ b/railties/guides/source/caching_with_rails.textile
@@ -2,7 +2,14 @@ h2. Caching with Rails: An overview
Everyone caches. This guide will teach you what you need to know about
avoiding that expensive round-trip to your database and returning what you
-need to return to those hungry web clients in the shortest time possible.
+need to return to those hungry web clients in the shortest time possible.
+
+After reading this guide, you should be able to use and configure:
+
+* Page, action, and fragment caching
+* Sweepers
+* Alternative cache stores
+* Conditional GET support
endprologue.
@@ -11,10 +18,11 @@ h3. Basic Caching
This is an introduction to the three types of caching techniques that Rails
provides by default without the use of any third party plugins.
-To get started make sure +config.action_controller.perform_caching+ is set
-to +true+ for your environment. This flag is normally set in the
+To start playing with testing you'll want to ensure that
++config.action_controller.perform_caching+ is set
+to +true+ if you're running in development mode. This flag is normally set in the
corresponding config/environments/*.rb and caching is disabled by default
-there for development and test, and enabled for production.
+ for development and test, and enabled for production.
<ruby>
config.action_controller.perform_caching = true
@@ -23,14 +31,14 @@ config.action_controller.perform_caching = true
h4. Page Caching
Page caching is a Rails mechanism which allows the request for a generated
-page to be fulfilled by the webserver, without ever having to go through the
+page to be fulfilled by the webserver (i.e. apache or nginx), without ever having to go through the
Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be
applied to every situation (such as pages that need authentication) and since
the webserver is literally just serving a file from the filesystem, cache
expiration is an issue that needs to be dealt with.
So, how do you enable this super-fast cache behavior? Simple, let's say you
-have a controller called ProductsController and a 'list' action that lists all
+have a controller called +ProductsController+ and an +index+ action that lists all
the products
<ruby>
@@ -38,20 +46,22 @@ class ProductsController < ActionController
caches_page :index
- def index; end
+ def index
+ @products = Products.all
+ end
end
</ruby>
-The first time anyone requests products/index, Rails will generate a file
-called +index.html+ and the webserver will then look for that file before it
-passes the next request for products/index to your Rails application.
+The first time anyone requests +/products+, Rails will generate a file
+called +products.html+ and the webserver will then look for that file before it
+passes the next request for +/products+ to your Rails application.
-By default, the page cache directory is set to Rails.public_path (which is
-usually set to +RAILS_ROOT + "/public"+) and this can be configured by
+By default, the page cache directory is set to +Rails.public_path+ (which is
+usually set to the +public+ folder) and this can be configured by
changing the configuration setting +config.action_controller.page_cache_directory+.
-Changing the default from /public helps avoid naming conflicts, since you may
-want to put other static html in /public, but changing this will require web
+Changing the default from +public+ helps avoid naming conflicts, since you may
+want to put other static html in +public+, but changing this will require web
server reconfiguration to let the web server know where to serve the cached
files from.
@@ -66,12 +76,14 @@ example controller like this:
<ruby>
class ProductsController < ActionController
- caches_page :list
+ caches_page :index
- def list; end
+ def index
+ @products = Products.all
+ end
def create
- expire_page :action => :list
+ expire_page :action => :index
end
end
@@ -80,7 +92,7 @@ end
If you want a more complicated expiration scheme, you can use cache sweepers
to expire cached objects when things change. This is covered in the section on Sweepers.
-Note: Page caching ignores all parameters, so /products/list?page=1 will be written out to the filesystem as /products/list.html and if someone requests /products/list?page=2, they will be returned the same result as page=1, so be careful when page caching GET parameters in the URL!
+Note: Page caching ignores all parameters. For example +/products?page=1+ will be written out to the filesystem as +products.html+ with no reference to the +page+ parameter. Thus, if someone requests +/products?page=2+ later, they will get the cached first page. Be careful when page caching GET parameters in the URL!
h4. Action Caching
@@ -88,47 +100,44 @@ One of the issues with Page Caching is that you cannot use it for pages that
require to restrict access somehow. This is where Action Caching comes in.
Action Caching works like Page Caching except for the fact that the incoming
web request does go from the webserver to the Rails stack and Action Pack so
-that before filters can be run on it before the cache is served, so that
-authentication and other restrictions can be used while still serving the
+that before filters can be run on it before the cache is served. This allows
+authentication and other restriction to be run while still serving the
result of the output from a cached copy.
Clearing the cache works in the exact same way as with Page Caching.
-Let's say you only wanted authenticated users to edit or create a Product
-object, but still cache those pages:
+Let's say you only wanted authenticated users to call actions on +ProductsController+.
<ruby>
class ProductsController < ActionController
- before_filter :authenticate, :only => [ :edit, :create ]
- caches_page :list
- caches_action :edit
+ before_filter :authenticate
+ caches_action :index
- def list; end
+ def index
+ @products = Product.all
+ end
def create
- expire_page :action => :list
- expire_action :action => :edit
+ expire_action :action => :index
end
- def edit; end
-
end
</ruby>
-And you can also use +:if+ (or +:unless+) to pass a Proc that specifies when the
+You can also use +:if+ (or +:unless+) to pass a Proc that specifies when the
action should be cached. Also, you can use +:layout => false+ to cache without
layout so that dynamic information in the layout such as logged in user info
or the number of items in the cart can be left uncached. This feature is
available as of Rails 2.2.
You can modify the default action cache path by passing a +:cache_path+ option.
-This will be passed directly to ActionCachePath.path_for. This is handy for
+This will be passed directly to +ActionCachePath.path_for+. This is handy for
actions with multiple possible routes that should be cached differently. If
a block is given, it is called with the current controller instance.
Finally, if you are using memcached, you can also pass +:expires_in+. In fact,
-all parameters not used by caches_action are sent to the underlying cache
+all parameters not used by +caches_action+ are sent to the underlying cache
store.
h4. Fragment Caching
@@ -144,7 +153,7 @@ Fragment Caching allows a fragment of view logic to be wrapped in a cache
block and served out of the cache store when the next request comes in.
As an example, if you wanted to show all the orders placed on your website
-in real time and didn't want to cache that part of the page, but did want
+in real time and didn't want to cache that part of the page, but did want
to cache the part of the page which lists all products available, you
could use this piece of code:
@@ -155,7 +164,7 @@ could use this piece of code:
<% cache do %>
All available products:
- <% Product.find(:all).each do |p| %>
+ <% Product.all.each do |p| %>
<%= link_to p.name, product_url(p) %>
<% end %>
<% end %>
@@ -173,40 +182,40 @@ want to cache multiple fragments per action, you should provide an +action_suffi
and you can expire it using the +expire_fragment+ method, like so:
<ruby>
-expire_fragment(:controller => 'products', :action => 'recent', :action_suffix => 'all_products)
+expire_fragment(:controller => 'products', :action => 'recent', :action_suffix => 'all_products')
</ruby>
If you don't want the cache block to bind to the action that called it, You can
-also use globally keyed fragments by calling the cache method with a key, like
+also use globally keyed fragments by calling the +cache+ method with a key, like
so:
<ruby>
-<% cache(:key => ['all_available_products', @latest_product.created_at].join(':')) do %>
+<% cache('all_available_products') do %>
All available products:
<% end %>
</ruby>
-This fragment is then available to all actions in the ProductsController using
+This fragment is then available to all actions in the +ProductsController+ using
the key and can be expired the same way:
<ruby>
-expire_fragment(:key => ['all_available_products', @latest_product.created_at].join(':'))
+expire_fragment('all_available_products')
</ruby>
h4. Sweepers
Cache sweeping is a mechanism which allows you to get around having a ton of
-expire_{page,action,fragment} calls in your code by moving all the work
++expire_{page,action,fragment}+ calls in your code. It does this by moving all the work
required to expire cached content into a +ActionController::Caching::Sweeper+
-class that is an Observer and looks for changes to an object via callbacks,
-and when a change occurs it expires the caches associated with that object n
+class. This class is an Observer and looks for changes to an object via callbacks,
+and when a change occurs it expires the caches associated with that object in
an around or after filter.
Continuing with our Product controller example, we could rewrite it with a
-sweeper such as the following:
+sweeper like this:
<ruby>
-class StoreSweeper < ActionController::Caching::Sweeper
+class ProductSweeper < ActionController::Caching::Sweeper
observe Product # This sweeper is going to keep an eye on the Product model
# If our sweeper detects that a Product was created call this
@@ -225,16 +234,24 @@ class StoreSweeper < ActionController::Caching::Sweeper
end
private
- def expire_cache_for(record)
- # Expire the list page now that we added a new product
- expire_page(:controller => '#{record}', :action => 'list')
+ def expire_cache_for(product)
+ # Expire the index page now that we added a new product
+ expire_page(:controller => 'products', :action => 'index')
# Expire a fragment
- expire_fragment(:controller => '#{record}', :action => 'recent', :action_suffix => 'all_products')
+ expire_fragment('all_available_products')
end
end
</ruby>
+You may notice that the actual product gets passed to the sweeper, so if we
+were caching the edit action for each product, we could add a expire method
+which specifies the page we want to expire:
+
+<ruby>
+ expire_action(:controller => 'products', :action => 'edit', :id => product)
+</ruby>
+
Then we add it to our controller to tell it to call the sweeper when certain
actions are called. So, if we wanted to expire the cached content for the
list and edit actions when the create action was called, we could do the
@@ -243,20 +260,14 @@ following:
<ruby>
class ProductsController < ActionController
- before_filter :authenticate, :only => [ :edit, :create ]
- caches_page :list
- caches_action :edit
- cache_sweeper :store_sweeper, :only => [ :create ]
+ before_filter :authenticate
+ caches_action :index
+ cache_sweeper :product_sweeper
- def list; end
-
- def create
- expire_page :action => :list
- expire_action :action => :edit
+ def index
+ @products = Product.all
end
- def edit; end
-
end
</ruby>
@@ -272,49 +283,38 @@ For example:
<ruby>
class ProductsController < ActionController
- before_filter :authenticate, :only => [ :edit, :create ]
- caches_page :list
- caches_action :edit
- cache_sweeper :store_sweeper, :only => [ :create ]
-
- def list
+ def index
# Run a find query
- Product.find(:all)
+ @products = Product.all
...
# Run the same query again
- Product.find(:all)
- end
-
- def create
- expire_page :action => :list
- expire_action :action => :edit
+ @products = Product.all
end
- def edit; end
-
end
</ruby>
-In the 'list' action above, the result set returned by the first
-Product.find(:all) will be cached and will be used to avoid querying the
-database again the second time that finder is called.
+The second time the same query is run against the database, it's not actually
+going to hit the database. The first time the result is returned from the query
+it is stored in the query cache (in memory) and the second time it's pulled from memory.
-Query caches are created at the start of an action and destroyed at the end of
-that action and thus persist only for the duration of the action.
+However, it's important to note that query caches are created at the start of an action and destroyed at the end of
+that action and thus persist only for the duration of the action. If you'd like to store query results in a more
+persistent fashion, you can in Rails by using low level caching.
h4. Cache stores
-Rails (as of 2.1) provides different stores for the cached data for action and
+Rails (as of 2.1) provides different stores for the cached data created by action and
fragment caches. Page caches are always stored on disk.
-Rails 2.1 and above provide ActiveSupport::Cache::Store which can be used to
+Rails 2.1 and above provide +ActiveSupport::Cache::Store+ which can be used to
cache strings. Some cache store implementations, like MemoryStore, are able to
cache arbitrary Ruby objects, but don't count on every cache store to be able
to do that.
-The default cache stores provided include:
+The default cache stores provided with Rails include:
1) ActiveSupport::Cache::MemoryStore: A cache store implementation which stores
everything into memory in the same process. If you're running multiple Ruby on
@@ -365,7 +365,7 @@ Special features:
and MemCacheStore will load balance between all available servers. If a
server goes down, then MemCacheStore will ignore it until it goes back
online.
- * Time-based expiry support. See write and the +:expires_in+ option.
+ * Time-based expiry support. See +write+ and the +:expires_in+ option.
* Per-request in memory cache for all communication with the MemCache server(s).
It also accepts a hash of additional options:
@@ -375,13 +375,12 @@ It also accepts a hash of additional options:
* +:multithread+ - a boolean value that adds thread safety to read/write operations - it is unlikely you'll need to use this option as the Rails threadsafe! method offers the same functionality.
The read and write methods of the MemCacheStore accept an options hash too.
-When reading you can specify +:raw => true+ to prevent the object being
-marshaled
+When reading you can specify +:raw => true+ to prevent the object being marshaled
(by default this is false which means the raw value in the cache is passed to
-Marshal.load before being returned to you.)
++Marshal.load+ before being returned to you.)
When writing to the cache it is also possible to specify +:raw => true+ means
-the value is not passed to Marshal.dump before being stored in the cache (by
+the value is not passed to +Marshal.dump+ before being stored in the cache (by
default this is false).
The write method also accepts an +:unless_exist+ flag which determines whether
@@ -416,15 +415,14 @@ ActionController::Base.cache_store = :compressed_mem_cache_store, "localhost"
ActionController::Base.cache_store = MyOwnStore.new("parameter")
</ruby>
-+Note: config.cache_store can be used in place of
-ActionController::Base.cache_store in your Rails::Initializer.run block in
-environment.rb+
++Note: +config.cache_store+ can be used in place of
++ActionController::Base.cache_store+ in your +Rails::Initializer.run+ block in
++environment.rb+
-In addition to all of this, Rails also adds the ActiveRecord::Base#cache_key
-method that generates a key using the class name, id and updated_at timestamp
-(if available).
+In addition to all of this, Rails also adds the +ActiveRecord::Base#cache_key+
+method that generates a key using the class name, +id+ and +updated_at+ timestamp (if available).
-An example:
+You can access these cache stores at a low level for storing queries and other objects. Here's an example:
<ruby>
Rails.cache.read("city") # => nil
@@ -434,16 +432,16 @@ Rails.cache.read("city") # => "Duckburgh"
h3. Conditional GET support
-Conditional GETs are a facility of the HTTP spec that provide a way for web
+Conditional GETs are a feature of the HTTP specification that provide a way for web
servers to tell browsers that the response to a GET request hasn't changed
since the last request and can be safely pulled from the browser cache.
-They work by using the HTTP_IF_NONE_MATCH and HTTP_IF_MODIFIED_SINCE headers to
-pass back and forth both a unique content identifier and the timestamp of when
-the content was last changed. If the browser makes a request where the content
-identifier (etag) or last modified since timestamp matches the server’s version
-then the server only needs to send back an empty response with a not modified
-status.
+They work by using the +HTTP_IF_NONE_MATCH+ and +HTTP_IF_MODIFIED_SINCE+ headers
+to pass back and forth both a unique content identifier and the timestamp of
+when the content was last changed. If the browser makes a request where the
+content identifier (etag) or last modified since timestamp matches the server’s
+version then the server only needs to send back an empty response with a not
+modified status.
It is the server's (i.e. our) responsibility to look for a last modified
timestamp and the if-none-match header and determine whether or not to send
@@ -501,16 +499,18 @@ Also the new "Cache money":http://github.com/nkallen/cache-money/tree/master plu
h3. References
+* "Scaling Rails Screencasts":http://railslab.newrelic.com/scaling-rails
* "RailsEnvy, Rails Caching Tutorial, Part 1":http://www.railsenvy.com/2007/2/28/rails-caching-tutorial
* "RailsEnvy, Rails Caching Tutorial, Part 1":http://www.railsenvy.com/2007/3/20/ruby-on-rails-caching-tutorial-part-2
* "ActiveSupport::Cache documentation":http://api.rubyonrails.org/classes/ActiveSupport/Cache.html
* "Rails 2.1 integrated caching tutorial":http://thewebfellas.com/blog/2008/6/9/rails-2-1-now-with-better-integrated-caching
-h3. Changelog
+h3. Changelog
"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/10-guide-to-caching
-* February 22, 2009: Beefed up the section on cache_stores
-* December 27, 2008: Typo fixes
-* November 23, 2008: Incremental updates with various suggested changes and formatting cleanup
-* September 15, 2008: Initial version by Aditya Chadha
+April 1, 2009: Made a bunch of small fixes
+February 22, 2009: Beefed up the section on cache_stores
+December 27, 2008: Typo fixes
+November 23, 2008: Incremental updates with various suggested changes and formatting cleanup
+September 15, 2008: Initial version by Aditya Chadha
diff --git a/railties/guides/source/command_line.textile b/railties/guides/source/command_line.textile
index 078eefd9df..d042458419 100644
--- a/railties/guides/source/command_line.textile
+++ b/railties/guides/source/command_line.textile
@@ -24,7 +24,7 @@ There are a few commands that are absolutely critical to your everyday usage of
Let's create a simple Rails application to step through each of these commands in context.
-h4. rails
+h4. +rails+
The first thing we'll want to do is create a new Rails application by running the +rails+ command after installing Rails.
@@ -48,7 +48,7 @@ Rails will set you up with what seems like a huge amount of stuff for such a tin
INFO: This output will seem very familiar when we get to the +generate+ command. Creepy foreshadowing!
-h4. server
+h4. +server+
Let's try it! The +server+ command launches a small web server named WEBrick which comes bundled with Ruby. You'll use this any time you want to view your work through a web browser.
@@ -71,7 +71,7 @@ WHOA. With just three commands we whipped up a Rails server listening on port 30
See? Cool! It doesn't do much yet, but we'll change that.
-h4. generate
+h4. +generate+
The +generate+ command uses templates to create a whole lot of things. You can always find out what's available by running +generate+ by itself. Let's do that:
@@ -248,15 +248,15 @@ Let's see the interface Rails created for us. ./script/server; http://localhost:
We can create new high scores (55,160 on Space Invaders!)
-h4. console
+h4. +console+
The +console+ command lets you interact with your Rails application from the command line. On the underside, +script/console+ uses IRB, so if you've ever used it, you'll be right at home. This is useful for testing out quick ideas with code and changing data server-side without touching the website.
-h4. dbconsole
+h4. +dbconsole+
+dbconsole+ figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL, PostgreSQL, SQLite and SQLite3.
-h4. plugin
+h4. +plugin+
The +plugin+ command simplifies plugin management; think a miniature version of the Gem utility. Let's walk through installing a plugin. You can call the sub-command *discover*, which sifts through repositories looking for plugins, or call *source* to add a specific repository of plugins, or you can specify the plugin location directly.
@@ -272,7 +272,7 @@ $ ./script/plugin install http://svn.techno-weenie.net/projects/plugins/acts_as_
...
</shell>
-h4. runner
+h4. +runner+
<tt>runner</tt> runs Ruby code in the context of Rails non-interactively. For instance:
@@ -280,7 +280,7 @@ h4. runner
$ ./script/runner "Model.long_running_method"
</shell>
-h4. destroy
+h4. +destroy+
Think of +destroy+ as the opposite of +generate+. It'll figure out what generate did, and undo it. Believe you-me, the creation of this tutorial used this command many times!
@@ -309,7 +309,7 @@ $ ./script/destroy model Oops
notempty app
</shell>
-h4. about
+h4. +about+
Check it: Version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version! +about+ is useful when you need to ask for help, check if a security patch might affect you, or when you need some stats for an existing Rails installation.
@@ -335,7 +335,7 @@ h3. The Rails Advanced Command Line
The more advanced uses of the command line are focused around finding useful (even surprising at times) options in the utilities, and fitting utilities to your needs and specific work flow. Listed here are some tricks up Rails' sleeve.
-h4. Rails with databases and SCM
+h4. Rails with Databases and SCM
When creating a new Rails application, you have the option to specify what kind of database and what kind of source code management system your application is going to use. This will save you a few minutes, and certainly many keystrokes.
@@ -393,7 +393,7 @@ development:
It also generated some lines in our database.yml configuration corresponding to our choice of PostgreSQL for database. The only catch with using the SCM options is that you have to make your application's directory first, then initialize your SCM, then you can run the +rails+ command to generate the basis of your app.
-h4. server with different backends
+h4. +server+ with Different Backends
Many people have created a large number different web servers in Ruby, and many of them can be used to run Rails. Since version 2.3, Rails uses Rack to serve its webpages, which means that any webserver that implements a Rack handler can be used. This includes WEBrick, Mongrel, Thin, and Phusion Passenger (to name a few!).
@@ -534,25 +534,25 @@ rake tmp:sockets:clear # Clears all files in tmp/sockets
Let's take a look at some of these 80 or so rake tasks.
-h5. db: Database
+h5. +db:+ Database
The most common tasks of the +db:+ Rake namespace are +migrate+ and +create+, and it will pay off to try out all of the migration rake tasks (+up+, +down+, +redo+, +reset+). +rake db:version+ is useful when troubleshooting, telling you the current version of the database.
-h5. doc: Documentation
+h5. +doc:+ Documentation
If you want to strip out or rebuild any of the Rails documentation (including this guide!), the +doc:+ namespace has the tools. Stripping documentation is mainly useful for slimming your codebase, like if you're writing a Rails application for an embedded platform.
-h5. gems: Ruby gems
+h5. +gems:+ Ruby gems
You can specify which gems your application uses, and +rake gems:install+ will install them for you. Look at your environment.rb to learn how with the *config.gem* directive.
NOTE: +gems:unpack+ will unpack, that is internalize your application's Gem dependencies by copying the Gem code into your vendor/gems directory. By doing this you increase your codebase size, but simplify installation on new hosts by eliminating the need to run +rake gems:install+, or finding and installing the gems your application uses.
-h5. notes: Code note enumeration
+h5. +notes:+ Code note enumeration
These tasks will search through your code for commented lines beginning with "FIXME", "OPTIMIZE", "TODO", or any custom annotation (like XXX) and show you them.
-h5. rails: Rails-specific tasks
+h5. +rails:+ Rails-specific tasks
In addition to the +gems:unpack+ task above, you can also unpack the Rails backend specific gems into vendor/rails by calling +rake rails:freeze:gems+, to unpack the version of Rails you are currently using, or +rake rails:freeze:edge+ to unpack the most recent (cutting, bleeding edge) version.
@@ -560,7 +560,7 @@ When you have frozen the Rails gems, Rails will prefer to use the code in vendor
After upgrading Rails, it is useful to run +rails:update+, which will update your config and scripts directories, and upgrade your Rails-specific javascript (like Scriptaculous).
-h5. test: Rails tests
+h5. +test:+ Rails tests
INFO: A good description of unit testing in Rails is given in "A Guide to Testing Rails Applications":testing.html
@@ -568,15 +568,15 @@ Rails comes with a test suite called Test::Unit. It is through the use of tests
The +test:+ namespace helps in running the different tests you will (hopefully!) write.
-h5. time: Timezones
+h5. +time:+ Timezones
You can list all the timezones Rails knows about with +rake time:zones:all+, which is useful just in day-to-day life.
-h5. tmp: Temporary files
+h5. +tmp:+ Temporary files
The tmp directory is, like in the *nix /tmp directory, the holding place for temporary files like sessions (if you're using a file store for files), process id files, and cached actions. The +tmp:+ namespace tasks will help you clear them if you need to if they've become overgrown, or create them in case of an +rm -rf *+ gone awry.
-h5. Miscellaneous tasks
+h5. Miscellaneous Tasks
+rake stats+ is great for looking at statistics on your code, displaying things like KLOCs (thousands of lines of code) and your code to test ratio.
@@ -584,3 +584,6 @@ h5. Miscellaneous tasks
+rake routes+ will list all of your defined routes, which is useful for tracking down routing problems in your app, or giving you a good overview of the URLs in an app you're trying to get familiar with.
+h3. Changelog
+
+"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213/tickets/29
diff --git a/railties/guides/source/contribute.textile b/railties/guides/source/contribute.textile
index 48f1a51d02..650004bd09 100644
--- a/railties/guides/source/contribute.textile
+++ b/railties/guides/source/contribute.textile
@@ -7,12 +7,12 @@ endprologue.
h3. How to Contribute?
* We have an open commit policy: anyone is welcome to contribute, but you'll need to ask for commit access.
-* PM lifo at "GitHub":http://github.om asking for "docrails":http://github.com/lifo/docrails commit access.
+* PM lifo at "GitHub":http://github.com asking for "docrails":http://github.com/lifo/docrails/tree/master commit access.
* Guides are written in Textile, and reside at railties/guides/source in the docrails project.
* All images are in the railties/guides/images directory.
* Sample format : "Active Record Associations":http://github.com/lifo/docrails/blob/3e56a3832415476fdd1cb963980d0ae390ac1ed3/railties/guides/source/association_basics.textile
* Sample output : "Active Record Associations":http://guides.rails.info/association_basics.html
-* You can build the Guides during testing by running railties/guides/rails_guides.rb.
+* You can build the Guides during testing by running +rake guides+ in the +railties+ directory.
h3. What to Contribute?
@@ -44,26 +44,21 @@ For each completed guide, the lead contributor will receive all of the following
h3. Rules
-* Guides are licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 License.
+* Guides are licensed under a Creative Commons Attribution-Share Alike 3.0 License.
* If you're not sure whether a guide is actively being worked on, stop by IRC and ask.
* If the same guide writer wants to write multiple guides, that's ideally the situation we'd love to be in! However, that guide writer will only receive the cash prize for all the subsequent guides (and not the GitHub or RPM prizes).
* Our review team will have the final say on whether the guide is complete and of good enough quality.
-h3. Reviewers
-
-These are the main reviewers and editors for the guides:
-
-* Hongli Lai
-* Mike Gunderloy
-* Pratik Naik
-* Xavier Noria
-
All authors should read and follow the "Rails Guides Conventions":http://wiki.github.com/lifo/docrails/rails-guides-conventions and the "Rails API Documentation Conventions":http://wiki.github.com/lifo/docrails/rails-api-documentation-conventions.
h3. Translations
The translation effort for the Rails Guides is just getting underway. We know about projects to translate the Guides into Spanish, Portuguese, Polish, and French. For more details or to get involved see the "Translating Rails Guides":http://wiki.github.com/lifo/docrails/translating-rails-guides page.
+h3. Mailing List
+
+"Ruby on Rails: Documentation":http://groups.google.com/group/rubyonrails-docs is the mailing list for all the guides/documentation related discussions.
+
h3. IRC Channel
==#docrails @ irc.freenode.net==
@@ -72,6 +67,5 @@ h3. Contact
If you have any questions or need any clarification, feel free to contact:
-* IRC : lifo, mikeg1a, fxn, or FooBarWidget in #docrails
+* IRC : lifo, mikeg1a or fxn in #docrails
* Email : pratiknaik aT gmail
-
diff --git a/railties/guides/source/contributing.textile b/railties/guides/source/contributing_to_rails.textile
index cba83a2809..84778ed9ee 100644
--- a/railties/guides/source/contributing.textile
+++ b/railties/guides/source/contributing_to_rails.textile
@@ -72,7 +72,16 @@ mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.*
If you’re using another database, check the files under +activerecord/test/connections+ in the Rails source code for default connection information. You can edit these files if you _must_ on your machine to provide different credentials, but obviously you should not push any such changes back to Rails.
-Now if you go back to the root of the Rails source on your machine and run +rake+ with no parameters, you should see every test in all of the Rails components pass. After that, check out the file +activerecord/RUNNING_UNIT_TESTS+ for information on running more targeted database tests, or the file +ci/ci_build.rb+ to see the test suite that the Rails continuous integration server runs.
+Now if you go back to the root of the Rails source on your machine and run +rake+ with no parameters, you should see every test in all of the Rails components pass. If you want to run the all ActiveRecord tests (or just a single one) with another database adapter, enter this from the +activerecord+ directory:
+
+<shell>
+rake test_sqlite3
+rake test_sqlite3 TEST=test/cases/validations_test.rb
+</shell>
+
+You can change +sqlite3+ with +jdbcmysql+, +jdbcsqlite3+, +jdbcpostgresql+, +mysql+ or +postgresql+. Check out the file +activerecord/RUNNING_UNIT_TESTS+ for information on running more targeted database tests, or the file +ci/ci_build.rb+ to see the test suite that the Rails continuous integration server runs.
+
+
NOTE: If you're working with Active Record code, you _must_ ensure that the tests pass for at least MySQL, PostgreSQL, SQLite 2, and SQLite 3. Subtle differences between the various Active Record database adapters have been behind the rejection of many patches that looked OK when tested only against MySQL.
@@ -121,19 +130,23 @@ If your comment simply says "+1", then odds are that other reviewers aren't goin
h3. Contributing to the Rails Documentation
-Another area where you can help out if you're not yet ready to take the plunge to writing Rails core code is with Rails documentation. You can help with the Rails wiki, the Rails API documentation, or the Rails Guides.
+Another area where you can help out if you're not yet ready to take the plunge to writing Rails core code is with Rails documentation. You can help with the Rails Guides or the Rails API documentation.
+
+TIP: "docrails":http://github.com/lifo/docrails/tree/master is the documentation branch for Rails with an *open commit policy*. You can simply PM "lifo":http://github.com/lifo on Github and ask for the commit rights. Documentation changes made as part of the "docrails":http://github.com/lifo/docrails/tree/master project, are merged back to the Rails master code from time to time. Check out the "original announcement":http://weblog.rubyonrails.org/2008/5/2/help-improve-rails-documentation-on-git-branch for more details.
-h4. The Rails Wiki
+h4. The Rails Guides
-The "Rails wiki":http://wiki.rubyonrails.org/ is a collection of user-generated and freely-editable documentation about Rails. It covers everything from getting started to FAQs to how-tos and popular plugins. To contribute to the wiki, just find some useful information that isn't there already and add it. There are style guidelines to help keep the wiki a coherent resources; see the section on "contributing to the wiki":http://wiki.rubyonrails.org/#contributing_to_the_wiki for more details.
+The "Rails Guides":http://guides.rubyonrails.org/ are a set of online resources that are designed to make people productive with Rails and to understand how all of the pieces fit together. These guides (including this one!) are written as part of the "docrails":http://github.com/lifo/docrails/tree/master project. If you have an idea for a new guide, or improvements for an existing guide, you can refer to the "contribution page":contribute.html for instructions on getting involved.
h4. The Rails API Documentation
-The "Rails API documentation":http://api.rubyonrails.org/ is automatically generated from the Rails source code via "RDoc":http://rdoc.rubyforge.org/. If you find some part of the documentation to be incomplete, confusing, or just plain wrong, you can step in and fix it. Documentation changes are made as part of the "docrails":http://github.com/lifo/docrails/tree/master project, which is merged back to the Rails master code from time to time. To contribute an update to the API documentation, you can send it as a patch to the "Rails Guides Lighthouse":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets?q=all. Alternatively, you can contact "lifo":http://github.com/lifo on GitHub and ask for commit rights to the docrails repository.
+The "Rails API documentation":http://api.rubyonrails.org/ is automatically generated from the Rails source code via "RDoc":http://rdoc.rubyforge.org/. If you find some part of the documentation to be incomplete, confusing, or just plain wrong, you can step in and fix it.
-h4. The Rails Guides
+To contribute an update to the API documentation, you can contact "lifo":http://github.com/lifo on GitHub and ask for commit rights to the docrails repository and push your changes to the docrails repository. Please follow the "docrails RDoc conventions":http://wiki.github.com/lifo/docrails/rails-api-documentation-conventions when contributing the changes.
+
+h3. The Rails Wiki
-The "Rails Guides":http://guides.rubyonrails.org/ are a set of online resources that are designed to make people productive with Rails and to understand how all of the pieces fit together. These guides (including this one!) are written as part of the docrails project. If you have an idea for a new guide, or improvements for an existing guide, you can refer to the "contribution page":http://guides.rails.info/contribute.html for instructions on getting involved.
+The "Rails wiki":http://wiki.rubyonrails.org/ is a collection of user-generated and freely-editable information about Rails. It covers everything from getting started to FAQs to how-tos and popular plugins. To contribute to the wiki, just find some useful information that isn't there already and add it. There are style guidelines to help keep the wiki a coherent resources; see the section on "contributing to the wiki":http://wiki.rubyonrails.org/#contributing_to_the_wiki for more details.
h3. Contributing to the Rails Code
diff --git a/railties/guides/source/credits.erb.textile b/railties/guides/source/credits.erb.textile
index f0e8a469f0..b09a931fd6 100644
--- a/railties/guides/source/credits.erb.textile
+++ b/railties/guides/source/credits.erb.textile
@@ -5,6 +5,28 @@ p. We'd like to thank the following people for their tireless contributions to t
<% end %>
+<h3 class="section">Rails Documentation Team</h3>
+
+<% author('Mike Gunderloy', 'mgunderloy') do %>
+ Mike Gunderloy is a consultant with "ActionRails":http://www.actionrails.com and also a member of the "Rails activism team":http://rubyonrails.org/activists . He brings 25 years of experience in a variety of languages to bear on his current work with Rails. His near-daily links and other blogging can be found at "A Fresh Cup":http://afreshcup.com and he "twitters":http://twitter.com/MikeG1 too much.
+<% end %>
+
+<% author('Pratik Naik', 'lifo') do %>
+ Pratik Naik is a Ruby on Rails consultant with "ActionRails":http://www.actionrails.com and also a member of the "Rails core team":http://rubyonrails.org/core. He maintains a blog at "has_many :bugs, :through => :rails":http://m.onkey.org and has an active "twitter account":http://twitter.com/lifo.
+<% end %>
+
+<% author('Xavier Noria', 'fxn', 'fxn.jpg') do %>
+ Xavier Noria has been around dynamic languages since 2000. He fell in love with Rails in 2005, and cofounded Rails-based software company <a href="http://www.aspgems.com">ASPgems</a> in mid-2006. Xavier is president of the <a href="http://www.srug.org/">Spanish Ruby Users Group</a> and has been involved in Rails in several ways. He enjoys combining his passion for Rails and his past life as a proofreader of math textbooks. Oh, he also "tweets":http://twitter.com/fxn!
+<% end %>
+
+<h3 class="section">Rails Guides Designers</h3>
+
+<% author('Jason Zimdars', 'jz') do %>
+ Jason Zimdars is an experienced creative director and web designer who has lead UI and UX design for numerous websites and web applications. You can see more of his design and writing at <a href="http://www.thinkcage.com/">Thinkcage.com</a> or follow him on <a href="http://twitter.com/JZ">Twitter</a>.
+<% end %>
+
+<h3 class="section">Rails Guides Authors</h3>
+
<% author('Frederick Cheung', 'fcheung') do %>
Frederick Cheung is Chief Wizard at Texperts where he has been using Rails since 2006. He is based in Cambridge (UK) and when not consuming fine ales he blogs at "spacevatican.org":http://www.spacevatican.org.
<% end %>
@@ -17,23 +39,14 @@ p. We'd like to thank the following people for their tireless contributions to t
Jeff Dean is a software engineer with "Pivotal Labs":http://pivotallabs.com.
<% end %>
-<% author('Mike Gunderloy', 'mgunderloy') do %>
- Mike Gunderloy is a consultant with "ActionRails":http://www.actionrails.com and "Lark Group":http://larkware.com and also a member of the "Rails activism team":http://rubyonrails.org/activists . He brings 25 years of experience in a variety of languages to bear on his current work with Rails. His near-daily links and other blogging can be found at "A Fresh Cup":http://afreshcup.com and he "twitters":http://twitter.com/MikeG1 too much.
-<% end %>
-
<% author('Cássio Marques', 'cmarques') do %>
Cássio Marques is a Brazilian software developer working with different programming languages such as Ruby, JavaScript, CPP and Java, as an independent consultant. He blogs at "/* CODIFICANDO */":http://cassiomarques.wordpress.com, which is mainly written in Portuguese, but will soon get a new section for posts with English translation.
<% end %>
-<% author('Pratik Naik', 'lifo') do %>
- Pratik Naik is a Ruby on Rails consultant with "ActionRails":http://www.actionrails.com and also a member of the "Rails core team":http://rubyonrails.com/core. He maintains a blog at "has_many :bugs, :through => :rails":http://m.onkey.org and has an active "twitter account":http://twitter.com/lifo.
-
<% author('Emilio Tagua', 'miloops') do %>
- Emilio Tagua -- a.k.a. miloops -- is an Argentinian entrepreneur, developer, open source contributor and Rails evangelist. Cofounder of "Eventioz":http://www.eventioz.com. He has been using Rails since 2006 and contributing since early 2008. Can be found at gmail, twitter, freenode, everywhere as miloops.
+ Emilio Tagua -- a.k.a. miloops -- is an Argentinian entrepreneur, developer, open source contributor and Rails evangelist. Cofounder of "Eventioz":http://eventioz.com. He has been using Rails since 2006 and contributing since early 2008. Can be found at gmail, twitter, freenode, everywhere as miloops.
<% end %>
<% author('Heiko Webers', 'hawe') do %>
Heiko Webers is the founder of "bauland42":http://www.bauland42.de, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the "Ruby on Rails Security Project":http://www.rorsecurity.info. After 10 years of desktop application development, Heiko has rarely looked back.
<% end %>
-
-<% end %>
diff --git a/railties/guides/source/debugging_rails_applications.textile b/railties/guides/source/debugging_rails_applications.textile
index b1d6db2e55..c059fdabf8 100644
--- a/railties/guides/source/debugging_rails_applications.textile
+++ b/railties/guides/source/debugging_rails_applications.textile
@@ -17,7 +17,7 @@ One common task is to inspect the contents of a variable. In Rails, you can do t
* +to_yaml+
* +inspect+
-h4. debug
+h4. +debug+
The +debug+ helper will return a &lt;pre&gt;-tag that renders the object using the YAML format. This will generate human-readable data from any object. For example, if you have this code in a view:
@@ -46,7 +46,7 @@ attributes_cache: {}
Title: Rails debugging guide
</yaml>
-h4. to_yaml
+h4. +to_yaml+
Displaying an instance variable, or any other object or method, in yaml format can be achieved this way:
@@ -76,7 +76,7 @@ attributes_cache: {}
Title: Rails debugging guide
</yaml>
-h4. inspect
+h4. +inspect+
Another useful method for displaying object values is +inspect+, especially when working with arrays or hashes. This will print the object value as a string. For example:
@@ -96,7 +96,7 @@ Will be rendered as follows:
Title: Rails debugging guide
</pre>
-h4. Debugging Javascript
+h4. Debugging JavaScript
Rails has built-in support to debug RJS, to active it, set +ActionView::Base.debug_rjs+ to _true_, this will specify whether RJS responses should be wrapped in a try/catch block that alert()s the caught exception (and then re-raises it).
@@ -118,7 +118,7 @@ h3. The Logger
It can also be useful to save information to log files at runtime. Rails maintains a separate log file for each runtime environment.
-h4. What is The Logger?
+h4. What is the Logger?
Rails makes use of Ruby's standard +logger+ to write log information. You can also substitute another logger such as +Log4R+ if you wish.
@@ -209,7 +209,7 @@ Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localh
Adding extra logging like this makes it easy to search for unexpected or unusual behavior in your logs. If you add extra logging, be sure to make sensible use of log levels, to avoid filling your production logs with useless trivia.
-h3. Debugging with ruby-debug
+h3. Debugging with +ruby-debug+
When your code is behaving in unexpected ways, you can try printing to logs or the console to diagnose the problem. Unfortunately, there are times when this sort of error tracking is not effective in finding the root cause of a problem. When you actually need to journey into your running source code, the debugger is your best companion.
diff --git a/railties/guides/source/form_helpers.textile b/railties/guides/source/form_helpers.textile
index 074aea300a..22d24b0903 100644
--- a/railties/guides/source/form_helpers.textile
+++ b/railties/guides/source/form_helpers.textile
@@ -1,4 +1,4 @@
-h2. Rails form helpers
+h2. Rails Form helpers
Forms in web applications are an essential interface for user input. However, form markup can quickly become tedious to write and maintain because of form control naming and their numerous attributes. Rails deals away with these complexities by providing view helpers for generating form markup. However, since they have different use-cases, developers are required to know all the differences between similar helper methods before putting them to use.
@@ -16,7 +16,7 @@ endprologue.
NOTE: This guide is not intended to be a complete documentation of available form helpers and their arguments. Please visit "the Rails API documentation":http://api.rubyonrails.org/ for a complete reference.
-h3. Dealing With Basic Forms
+h3. Dealing with Basic Forms
The most basic form helper is +form_tag+.
@@ -43,7 +43,7 @@ If you carefully observe this output, you can see that the helper generated some
NOTE: Throughout this guide, this +div+ with the hidden input will be stripped away to have clearer code samples.
-h4. A Generic search form
+h4. A Generic Search Form
Probably the most minimal form often seen on the web is a search form with a single text input for search terms. This form consists of:
@@ -82,7 +82,7 @@ Besides +text_field_tag+ and +submit_tag+, there is a similar helper for _every_
TIP: For every form input, an ID attribute is generated from its name ("q" in the example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript.
-h4. Multiple hashes in form helper calls
+h4. Multiple Hashes in Form Helper Calls
By now you've seen that the +form_tag+ helper accepts 2 arguments: the path for the action and an options hash. This hash specifies the method of form submission and HTML options such as the form element's class.
@@ -104,7 +104,7 @@ This is a common pitfall when using form helpers, since many of them accept mult
WARNING: Do not delimit the second hash without doing so with the first hash, otherwise your method invocation will result in an +expecting tASSOC+ syntax error.
-h4. Helpers for generating form elements
+h4. Helpers for Generating Form Elements
Rails provides a series of helpers for generating form elements such as checkboxes, text fields, radio buttons, and so on. These basic helpers, with names ending in <notextile>_tag</notextile> such as +text_field_tag+, +check_box_tag+, etc., generate just a single +&lt;input&gt;+ element. The first parameter to these is always the name of the input. In the controller this name will be the key in the +params+ hash used to get the value entered by the user. For example, if the form contains
@@ -140,7 +140,7 @@ output:
The second parameter to +check_box_tag+ is the value of the input. This is the value that will be submitted by the browser if the checkbox is ticked (i.e. the value that will be present in the +params+ hash). With the above form you would check the value of +params[:pet_dog]+ and +params[:pet_cat]+ to see which pets the user owns.
-h5. Radio buttons
+h5. Radio Buttons
Radio buttons, while similar to checkboxes, are controls that specify a set of options in which they are mutually exclusive (i.e. the user can only pick one):
@@ -162,7 +162,7 @@ As with +check_box_tag+ the second parameter to +radio_button_tag+ is the value
IMPORTANT: Always use labels for each checkbox and radio button. They associate text with a specific option and provide a larger clickable region.
-h4. Other helpers of interest
+h4. Other Helpers of Interest
Other form controls worth mentioning are the text area, password input and hidden input:
@@ -183,9 +183,9 @@ Hidden inputs are not shown to the user, but they hold data like any textual inp
TIP: If you're using password input fields (for any purpose), you might want to prevent their values showing up in application logs by activating +filter_parameter_logging(:password)+ in your ApplicationController.
-h3. Dealing With Model Objects
+h3. Dealing with Model Objects
-h4. Model object helpers
+h4. Model Object Helpers
A particularly common task for a form is editing or creating a model object. While the +*_tag+ helpers can certainly be used for this task they are somewhat verbose as for each tag you would have to ensure the correct parameter name is used and set the default value of the input appropriately. Rails provides helpers tailored to this task. These helpers lack the <notextile>_tag</notextile> suffix, for example +text_field+, +text_area+.
@@ -207,7 +207,7 @@ WARNING: You must pass the name of an instance variable, i.e. +:person+ or +"per
Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the "Active Record Validations and Callbacks":./activerecord_validations_callbacks.html#_using_the_tt_errors_tt_collection_in_your_view_templates guide.
-h4. Binding a form to an object
+h4. Binding a Form to an Object
While this is an increase in comfort it is far from perfect. If Person has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what +form_for+ does.
@@ -276,7 +276,7 @@ which produces the following output:
The object yielded by +fields_for+ is a form builder like the one yielded by +form_for+ (in fact +form_for+ calls +fields_for+ internally).
-h4. Relying on record identification
+h4. Relying on Record Identification
The Article model is directly available to users of the application, so -- following the best practices for developing with Rails -- you should declare it *a resource*.
@@ -302,7 +302,7 @@ Rails will also automatically set the +class+ and +id+ of the form appropriately
WARNING: When you're using STI (single-table inheritance) with your models, you can't rely on record identification on a subclass if only their parent class is declared a resource. You will have to specify the model name, +:url+, and +:method+ explicitly.
-h5. Dealing with namespaces
+h5. Dealing with Namespaces
If you have created namespaced routes, +form_for+ has a nifty shorthand for that too. If your application has an admin namespace then
@@ -343,7 +343,7 @@ output:
When parsing POSTed data, Rails will take into account the special +_method+ parameter and acts as if the HTTP method was the one specified inside it ("PUT" in this example).
-h3. Making select boxes with ease
+h3. Making Select Boxes with Ease
Select boxes in HTML require a significant amount of markup (one +OPTION+ element for each option to choose from), therefore it makes the most sense for them to be dynamically generated.
@@ -360,7 +360,7 @@ Here is what the markup might look like:
Here you have a list of cities whose names are presented to the user. Internally the application only wants to handle their IDs so they are used as the options' value attribute. Let's see how Rails can help out here.
-h4. The select and options tag
+h4. The Select and Option Tags
The most generic helper is +select_tag+, which -- as the name implies -- simply generates the +SELECT+ tag that encapsulates an options string:
@@ -404,7 +404,7 @@ Whenever Rails sees that the internal value of an option being generated matches
TIP: The second argument to +options_for_select+ must be exactly equal to the desired internal value. In particular if the value is the integer 2 you cannot pass "2" to +options_for_select+ -- you must pass 2. Be aware of values extracted from the +params+ hash as they are all strings.
-h4. Select boxes for dealing with models
+h4. Select Boxes for Dealing with Models
In most cases form controls will be tied to a specific database model and as you might expect Rails provides helpers tailored for that purpose. Consistent with other form helpers, when dealing with models you drop the +_tag+ suffix from +select_tag+:
@@ -429,7 +429,7 @@ As with other helpers, if you were to use the +select+ helper on a form builder
WARNING: If you are using +select+ (or similar helpers such as +collection_select+, +select_tag+) to set a +belongs_to+ association you must pass the name of the foreign key (in the example above +city_id+), not the name of association itself. If you specify +city+ instead of +city_id+ Active Record will raise an error along the lines of <pre> ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750) </pre> when you pass the +params+ hash to +Person.new+ or +update_attributes+. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly. You may wish to consider the use of +attr_protected+ and +attr_accessible+. For further details on this, see the "Ruby On Rails Security Guide":security.html#_mass_assignment.
-h4. Option tags from a collection of arbitrary objects
+h4. Option Tags from a Collection of Arbitrary Objects
Generating options tags with +options_for_select+ requires that you create an array containing the text and value for each option. But what if you had a City model (perhaps an Active Record one) and you wanted to generate option tags from a collection of those objects? One solution would be to make a nested array by iterating over them:
@@ -454,7 +454,7 @@ To recap, +options_from_collection_for_select+ is to +collection_select+ what +o
NOTE: Pairs passed to +options_for_select+ should have the name first and the id second, however with +options_from_collection_for_select+ the first argument is the value method and the second the text method.
-h4. Time zone and country select
+h4. Time Zone and Country Select
To leverage time zone support in Rails, you have to ask your users what time zone they are in. Doing so would require generating select options from a list of pre-defined TimeZone objects using +collection_select+, but you can simply use the +time_zone_select+ helper that already wraps this:
@@ -475,7 +475,7 @@ The date and time helpers differ from all the other form helpers in two importan
Both of these families of helpers will create a series of select boxes for the different components (year, month, day etc.).
-h4. Barebones helpers
+h4. Barebones Helpers
The +select_*+ family of helpers take as their first argument an instance of Date, Time or DateTime that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example
@@ -499,7 +499,7 @@ Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, pa
The +:prefix+ option is the key used to retrieve the hash of date components from the +params+ hash. Here it was set to +start_date+, if omitted it will default to +date+.
-h4. Model object helpers
+h4. Model Object Helpers
+select_date+ does not work well with forms that update or create Active Record objects as Active Record expects each element of the +params+ hash to correspond to one attribute.
The model object helpers for dates and times submit parameters with special names, when Active Record sees parameters with such names it knows they must be combined with the other parameters and given to a constructor appropriate to the column type. For example:
@@ -524,7 +524,7 @@ which results in a +params+ hash like
When this is passed to +Person.new+ (or +update_attributes+), Active Record spots that these parameters should all be used to construct the +birth_date+ attribute and uses the suffixed information to determine in which order it should pass these parameters to functions such as +Date.civil+.
-h4. Common options
+h4. Common Options
Both families of helpers use the same core set of functions to generate the individual select tags and so both accept largely the same options. In particular, by default Rails will generate year options 5 years either side of the current year. If this is not an appropriate range, the +:start_year+ and +:end_year+ options override this. For an exhaustive list of the available options, refer to the "API documentation":http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html.
@@ -532,7 +532,7 @@ As a rule of thumb you should be using +date_select+ when working with model obj
NOTE: In many cases the built-in date pickers are clumsy as they do not aid the user in working out the relationship between the date and the day of the week.
-h4. Individual components
+h4. Individual Components
Occasionally you need to display just a single date component such as a year or a month. Rails provides a series of helpers for this, one for each component +select_year+, +select_month+, +select_day+, +select_hour+, +select_minute+, +select_second+. These helpers are fairly straightforward. By default they will generate an input field named after the time component (for example "year" for +select_year+, "month" for +select_month+ etc.) although this can be overriden with the +:field_name+ option. The +:prefix+ option works in the same way that it does for +select_date+ and +select_time+ and has the same default value.
@@ -563,7 +563,7 @@ The following two forms both upload a file.
Rails provides the usual pair of helpers: the barebones +file_field_tag+ and the model oriented +file_field+. The only difference with other helpers is that you cannot set a default value for file inputs as this would have no meaning. As you would expect in the first case the uploaded file is in +params[:picture]+ and in the second case in +params[:person][:picture]+.
-h4. What gets uploaded
+h4. What Gets Uploaded
The object in the +params+ hash is an instance of a subclass of IO. Depending on the size of the uploaded file it may in fact be a StringIO or an instance of File backed by a temporary file. In both cases the object will have an +original_filename+ attribute containing the name the file had on the user's computer and a +content_type+ attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in +#{Rails.root}/public/uploads+ under the same name as the original file (assuming the form was the one in the previous example).
@@ -631,7 +631,7 @@ Fundamentally HTML forms don't know about any sort of structured data, all they
TIP: You may find you can try out examples in this section faster by using the console to directly invoke Rails' parameter parser. For example <pre> ActionController::UrlEncodedPairParser.parse_query_parameters "name=fred&phone=0123456789" # => {"name"=>"fred", "phone"=>"0123456789"} </pre>
-h4. Basic structures
+h4. Basic Structures
The two basic structures are arrays and hashes. Hashes mirror the syntax used for accessing the value in +params+. For example if a form contains
@@ -669,7 +669,7 @@ Normally Rails ignores duplicate parameter names. If the parameter name contains
This would result in +params[:person][:phone_number]+ being an array.
-h4. Combining them
+h4. Combining Them
We can mix and match these two concepts. For example, one element of a hash might be an array as in the previous example, or you can have an array of hashes. For example a form might let you create any number of addresses by repeating the following form fragment
@@ -683,9 +683,9 @@ This would result in +params[:addresses]+ being an array of hashes with keys +li
There's a restriction, however, while hashes can be nested arbitrarily, only one level of "arrayness" is allowed. Arrays can be usually replaced by hashes, for example instead of having an array of model objects one can have a hash of model objects keyed by their id, an array index or some other parameter.
-WARNING: Array parameters do not play well with the +check_box+ helper. According to the HTML specification unchecked checkboxes submit no value. However it is often convenient for a checkbox to always submit a value. The +check_box+ helper fakes this by creating a second hidden input with the same name. If the checkbox is unchecked only the hidden input is submitted and if it is checked then both are submitted but the value submitted by the checkbox takes precedence. When working with array parameters this duplicate submission will confuse Rails since duplicate input names are how it decides when to start a new array element. It is preferable to either use +check_box_tag+ or to use hashes instead of arrays.
+WARNING: Array parameters do not play well with the +check_box+ helper. According to the HTML specification unchecked checkboxes submit no value. However it is often convenient for a checkbox to always submit a value. The +check_box+ helper fakes this by creating an auxiliary hidden input with the same name. If the checkbox is unchecked only the hidden input is submitted and if it is checked then both are submitted but the value submitted by the checkbox takes precedence. When working with array parameters this duplicate submission will confuse Rails since duplicate input names are how it decides when to start a new array element. It is preferable to either use +check_box_tag+ or to use hashes instead of arrays.
-h4. Using form helpers
+h4. Using Form Helpers
The previous sections did not use the Rails form helpers at all. While you can craft the input names yourself and pass them directly to helpers such as +text_field_tag+ Rails also provides higher level support. The two tools at your disposal here are the name parameter to +form_for+ and +fields_for+ and the +:index+ option that helpers take.
@@ -746,7 +746,7 @@ As a shortcut you can append [] to the name and omit the +:index+ option. This i
produces exactly the same output as the previous example.
-h3. Building Complex forms
+h3. Building Complex Forms
Many apps grow beyond simple forms editing a single object. For example when creating a Person you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary. While this guide has shown you all the pieces necessary to handle this, Rails does not yet have a standard end-to-end way of accomplishing this, but many have come up with viable approaches. These include:
diff --git a/railties/guides/source/getting_started.textile b/railties/guides/source/getting_started.textile
index 6e02cfe1bd..a216201490 100644
--- a/railties/guides/source/getting_started.textile
+++ b/railties/guides/source/getting_started.textile
@@ -1,4 +1,4 @@
-h2. Getting Started With Rails
+h2. Getting Started with Rails
This guide covers getting up and running with Ruby on Rails. After reading it, you should be familiar with:
@@ -23,7 +23,7 @@ It is highly recommended that you *familiarize yourself with Ruby before diving
* "Mr. Neighborly’s Humble Little Ruby Book":http://www.humblelittlerubybook.com
* "Programming Ruby":http://www.rubycentral.com/book
-* "Why’s (Poignant) Guide to Ruby":http://poignantguide.net/ruby
+* "Why’s (Poignant) Guide to Ruby":http://poignantguide.net/ruby/
h3. What is Rails?
@@ -175,7 +175,7 @@ In any case, Rails will create a folder in your working directory called <tt>blo
|log/|Application log files.|
|public/|The only folder seen to the world as-is. This is where your images, javascript, stylesheets (CSS), and other static files go.|
|script/|Scripts provided by Rails to do recurring tasks, such as benchmarking, plugin installation, and starting the console or the web server.|
-|test/|Unit tests, fixtures, and other test apparatus. These are covered in "Testing Rails Applications":testing_rails_applications.html|
+|test/|Unit tests, fixtures, and other test apparatus. These are covered in "Testing Rails Applications":testing.html|
|tmp/|Temporary files|
|vendor/|A place for third-party code. In a typical Rails application, this includes Ruby Gems, the Rails source code (if you install it into your project) and plugins containing additional prepackaged functionality.|
@@ -310,9 +310,9 @@ This line illustrates one tiny bit of the "convention over configuration" approa
Now if you navigate to +http://localhost:3000+ in your browser, you'll see the +home/index+ view.
-NOTE. For more information about routing, refer to "Rails Routing from the Outside In":routing_outside_in.html.
+NOTE. For more information about routing, refer to "Rails Routing from the Outside In":routing.html.
-h3. Getting Up and Running Quickly With Scaffolding
+h3. Getting Up and Running Quickly with Scaffolding
Rails _scaffolding_ is a quick way to generate some of the major pieces of an application. If you want to create the models, views, and controllers for a new resource in a single operation, scaffolding is the tool for the job.
@@ -472,9 +472,9 @@ end
This code sets the +@posts+ instance variable to an array of all posts in the database. +Post.find(:all)+ or +Post.all+ calls the +Post+ model to return all of the posts that are currently in the database, with no limiting conditions.
-TIP: For more information on finding records with Active Record, see "Active Record Finders":finders.html.
+TIP: For more information on finding records with Active Record, see "Active Record Query Interface":active_record_querying.html.
-The +respond_to+ block handles both HTML and XML calls to this action. If you browse to +http://localhost:3000/posts.xml+, you'll see all of the posts in XML format. The HTML format looks for a view in +app/views/posts/+ with a name that corresponds to the action name. Rails makes all of the instance variables from the action available to the view. Here's +app/view/posts/index.html.erb+:
+The +respond_to+ block handles both HTML and XML calls to this action. If you browse to +http://localhost:3000/posts.xml+, you'll see all of the posts in XML format. The HTML format looks for a view in +app/views/posts/+ with a name that corresponds to the action name. Rails makes all of the instance variables from the action available to the view. Here's +app/views/posts/index.html.erb+:
<erb>
<h1>Listing posts</h1>
@@ -846,7 +846,7 @@ end
Rails runs _before filters_ before any action in the controller. You can use the +:only+ clause to limit a before filter to only certain actions, or an +:except+ clause to specifically skip a before filter for certain actions. Rails also allows you to define _after filters_ that run after processing an action, as well as _around filters_ that surround the processing of actions. Filters can also be defined in external classes to make it easy to share them between controllers.
-For more information on filters, see the "Action Controller Basics":actioncontroller_basics.html guide.
+For more information on filters, see the "Action Controller Overview":action_controller_overview.html guide.
h3. Adding a Second Model
diff --git a/railties/guides/source/i18n.textile b/railties/guides/source/i18n.textile
index 4d9232917d..103ccb1c7a 100644
--- a/railties/guides/source/i18n.textile
+++ b/railties/guides/source/i18n.textile
@@ -12,37 +12,37 @@ So, in the process of _internationalizing_ your Rails application you have to:
In the process of _localizing_ your application you'll probably want to do following three things:
-* Replace or supplement Rails' default locale -- eg. date and time formats, month names, ActiveRecord model names, etc
-* Abstract texts in your application into keyed dictionaries -- eg. flash messages, static texts in your views, etc
+* Replace or supplement Rails' default locale -- e.g. date and time formats, month names, Active Record model names, etc
+* Abstract strings in your application into keyed dictionaries -- e.g. flash messages, static text in your views, etc.
* Store the resulting dictionaries somewhere
This guide will walk you through the I18n API and contains a tutorial how to internationalize a Rails application from the start.
endprologue.
-NOTE: The Ruby I18n framework provides you with all neccessary means for internationalization/localization of your Rails application. You may, however, use any of various plugins and extensions available, which add additional functionality or features. See Rails "I18n Wiki":http://rails-i18n.org/wiki for more information.
+NOTE: The Ruby I18n framework provides you with all necessary means for internationalization/localization of your Rails application. You may, however, use any of various plugins and extensions available, which add additional functionality or features. See the Rails "I18n Wiki":http://rails-i18n.org/wiki for more information.
-h3. How I18n in Ruby on Rails works
+h3. How I18n in Ruby on Rails Works
-Internationalization is a complex problem. Natural languages differ in so many ways (eg. in pluralization rules) that it is hard to provide tools for solving all problems at once. For that reason the Rails I18n API focuses on:
+Internationalization is a complex problem. Natural languages differ in so many ways (e.g. in pluralization rules) that it is hard to provide tools for solving all problems at once. For that reason the Rails I18n API focuses on:
* providing support for English and similar languages out of the box
* making it easy to customize and extend everything for other languages
-As part of this solution, *every static string in the Rails framework* -- eg. Active Record validation messages, time and date formats -- *has been internationalized*, so _localization_ of a Rails application means "over-riding" these defaults.
+As part of this solution, *every static string in the Rails framework* -- e.g. Active Record validation messages, time and date formats -- *has been internationalized*, so _localization_ of a Rails application means "over-riding" these defaults.
-h4. The overall architecture of the library
+h4. The Overall Architecture of the Library
Thus, the Ruby I18n gem is split into two parts:
-* The public API of the i18n framework -- a Ruby module with public methods and definitions how the library works
+* The public API of the i18n framework -- a Ruby module with public methods that define how the library works
* A default backend (which is intentionally named _Simple_ backend) that implements these methods
As a user you should always only access the public methods on the I18n module, but it is useful to know about the capabilities of the backend.
-NOTE: It is possible (or even desirable) to swap the shipped Simple backend with a more powerful one, which would store translation data in a relational database, GetText dictionary, or similar. See section "Using different backends":#usingdifferentbackends below.
+NOTE: It is possible (or even desirable) to swap the shipped Simple backend with a more powerful one, which would store translation data in a relational database, GetText dictionary, or similar. See section "Using different backends":#using-different-backends below.
-h4. The public I18n API
+h4. The Public I18n API
The most important methods of the I18n API are:
@@ -70,34 +70,34 @@ backend # Use a different backend
So, let's internationalize a simple Rails application from the ground up in the next chapters!
-h3. Setup the Rails application for internationalization
+h3. Setup the Rails Application for Internationalization
-There are just a few, simple steps to get up and running with I18n support for your application.
+There are just a few simple steps to get up and running with I18n support for your application.
-h4. Configure the I18n module
+h4. Configure the I18n Module
-Following the _convention over configuration_ philosophy, Rails will set-up your application with reasonable defaults. If you need different settings, you can overwrite them easily.
+Following the _convention over configuration_ philosophy, Rails will set up your application with reasonable defaults. If you need different settings, you can overwrite them easily.
-Rails adds all +.rb+ and +.yml+ files from +config/locales+ directory to your *translations load path*, automatically.
+Rails adds all +.rb+ and +.yml+ files from the +config/locales+ directory to your *translations load path*, automatically.
-See the default +en.yml+ locale in this directory, containing a sample pair of translation strings:
+The default +en.yml+ locale in this directory contains a sample pair of translation strings:
<ruby>
en:
hello: "Hello world"
</ruby>
-This means, that in the +:en+ locale, the key _hello_ will map to _Hello world_ string. Every string inside Rails is internationalized in this way, see for instance Active Record validation messages in the "+activerecord/lib/active_record/locale/en.yml+":http://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml file or time and date formats in the "+activesupport/lib/active_support/locale/en.yml+":http://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml file. You can use YAML or standard Ruby Hashes to store translations in the default (Simple) backend.
+This means, that in the +:en+ locale, the key _hello_ will map to the _Hello world_ string. Every string inside Rails is internationalized in this way, see for instance Active Record validation messages in the "+activerecord/lib/active_record/locale/en.yml+":http://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml file or time and date formats in the "+activesupport/lib/active_support/locale/en.yml+":http://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml file. You can use YAML or standard Ruby Hashes to store translations in the default (Simple) backend.
-The I18n library will use *English* as a *default locale*, ie. if you don't set a different locale, +:en+ will be used for looking up translations.
+The I18n library will use *English* as a *default locale*, i.e. if you don't set a different locale, +:en+ will be used for looking up translations.
-NOTE: The i18n library takes *pragmatic approach* to locale keys (after "some discussion":http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en), including only the _locale_ ("language") part, like +:en+, +:pl+, not the _region_ part, like +:en-US+ or +:en-UK+, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as +:cz+, +:th+ or +:es+ (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the +:en-US+ locale you would have $ as a currency symbol, while in +:en-UK+, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a +:en-UK+ dictionary. Various "Rails I18n plugins":http://rails-i18n.org/wiki such as "Globalize2":http://github.com/joshmh/globalize2 may help you implement it.
+NOTE: The i18n library takes a *pragmatic approach* to locale keys (after "some discussion":http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en), including only the _locale_ ("language") part, like +:en+, +:pl+, not the _region_ part, like +:en-US+ or +:en-UK+, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as +:cz+, +:th+ or +:es+ (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the +:en-US+ locale you would have $ as a currency symbol, while in +:en-UK+, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a +:en-UK+ dictionary. Various "Rails I18n plugins":http://rails-i18n.org/wiki such as "Globalize2":http://github.com/joshmh/globalize2/tree/master may help you implement it.
The *translations load path* (+I18n.load_path+) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you.
NOTE: The backend will lazy-load these translations when a translation is looked up for the first time. This makes it possible to just swap the backend with something else even after translations have already been announced.
-The default +environment.rb+ files has instruction how to add locales from another directory and how to set different default locale. Just uncomment and edit the specific lines.
+The default +environment.rb+ files has instruction how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines.
<ruby>
# The internationalization framework can be changed
@@ -107,31 +107,32 @@ The default +environment.rb+ files has instruction how to add locales from anoth
# config.i18n.default_locale = :de
</ruby>
-h4. Optional: custom I18n configuration setup
+h4. Optional: Custom I18n Configuration Setup
For the sake of completeness, let's mention that if you do not want to use the +environment.rb+ file for some reason, you can always wire up things manually, too.
-To tell the I18n library where it can find your custom translation files you can specify the load path anywhere in your application - just make sure it gets run before any translations are actually looked up. You might also want to change the default locale. The simplest thing possible is to put the following into an *initializer*:
+To tell the I18n library where it can find your custom translation files you can specify the load path anywhere in your application - just make sure it gets run before any translations are actually looked up. You might also want to change the default locale. The simplest thing possible is to put the following into an initializer:
<ruby>
# in config/initializer/locale.rb
# tell the I18n library where to find your translations
-I18n.load_path << Dir[ File.join(RAILS_ROOT, 'lib', 'locale', '*.{rb,yml}') ]
+I18n.load_path << Dir[ File.join(RAILS_ROOT, 'lib', 'locale',
+ '*.{rb,yml}') ]
-# set default locale to something else then :en
+# set default locale to something other than :en
I18n.default_locale = :pt
</ruby>
-h4. Setting and passing the locale
+h4. Setting and Passing the Locale
If you want to translate your Rails application to a *single language other than English* (the default locale), you can set I18n.default_locale to your locale in +environment.rb+ or an initializer as shown above, and it will persist through the requests.
However, you would probably like to *provide support for more locales* in your application. In such case, you need to set and pass the locale between requests.
-WARNING: You may be tempted to store choosed locale in a _session_ or a _cookie_. *Do not do so*. The locale should be transparent and a part of the URL. This way you don't break people's basic assumptions about the web itself: if you send a URL of some page to a friend, she should see the same page, same content. A fancy word for this would be that you're being "_RESTful_":http://en.wikipedia.org/wiki/Representational_State_Transfer. Read more about RESTful approach in "Stefan Tilkov's articles":http://www.infoq.com/articles/rest-introduction. There may be some exceptions to this rule, which are discussed below.
+WARNING: You may be tempted to store the chosen locale in a _session_ or a _cookie_. *Do not do so*. The locale should be transparent and a part of the URL. This way you don't break people's basic assumptions about the web itself: if you send a URL of some page to a friend, she should see the same page, same content. A fancy word for this would be that you're being "_RESTful_":http://en.wikipedia.org/wiki/Representational_State_Transfer. Read more about the RESTful approach in "Stefan Tilkov's articles":http://www.infoq.com/articles/rest-introduction. There may be some exceptions to this rule, which are discussed below.
-The _setting part_ is easy. You can set locale in a +before_filter+ in the ApplicationController like this:
+The _setting part_ is easy. You can set the locale in a +before_filter+ in the ApplicationController like this:
<ruby>
before_filter :set_locale
@@ -141,13 +142,13 @@ def set_locale
end
</ruby>
-This requires you to pass the locale as a URL query parameter as in +http://example.com/books?locale=pt+. (This is eg. Google's approach). So +http://localhost:3000?locale=pt+ will load the Portugese localization, whereas +http://localhost:3000?locale=de+ would load the German localization, and so on. You may skip the next section and head over to the *Internationalize your application* section, if you want to try things out by manually placing locale in the URL and reloading the page.
+This requires you to pass the locale as a URL query parameter as in +http://example.com/books?locale=pt+. (This is, for example, Google's approach.) So +http://localhost:3000?locale=pt+ will load the Portugese localization, whereas +http://localhost:3000?locale=de+ would load the German localization, and so on. You may skip the next section and head over to the *Internationalize your application* section, if you want to try things out by manually placing the locale in the URL and reloading the page.
-Of course, you probably don't want to manually include locale in every URL all over your application, or want the URLs look differently, eg. the usual +http://example.com/pt/books+ versus +http://example.com/en/books+. Let's discuss the different options you have.
+Of course, you probably don't want to manually include the locale in every URL all over your application, or want the URLs look differently, e.g. the usual +http://example.com/pt/books+ versus +http://example.com/en/books+. Let's discuss the different options you have.
-IMPORTANT: Following examples rely on having locales loaded into your application available as an array of strings like +["en", "es", "gr"]+. This is not inclued in current version of Rails 2.2 -- forthcoming Rails version 2.3 will contain easy accesor +available_locales+. (See "this commit":http://github.com/svenfuchs/i18n/commit/411f8fe7 and background at "Rails I18n Wiki":http://rails-i18n.org/wiki/pages/i18n-available_locales.)
+IMPORTANT: The following examples rely on having available locales loaded into your application as an array of strings like +["en", "es", "gr"]+. This is not included in the current version of Rails 2.2 -- the forthcoming Rails version 2.3 will contain the easy accessor +available_locales+. (See "this commit":http://github.com/svenfuchs/i18n/commit/411f8fe7c8f3f89e9b6b921fa62ed66cb92f3af4 and background at "Rails I18n Wiki":http://rails-i18n.org/wiki/pages/i18n-available_locales.)
-So, for having available locales easily available in Rails 2.2, we have to include this support manually in an initializer, like this:
+So, for having available locales easily accessible in Rails 2.2, we have to include this support manually in an initializer, like this:
<ruby>
# config/initializers/available_locales.rb
@@ -180,11 +181,11 @@ class ApplicationController < ActionController::Base
end
</ruby>
-h4. Setting locale from the domain name
+h4. Setting the Locale from the Domain Name
-One option you have is to set the locale from the domain name where your application runs. For example, we want +www.example.com+ to load English (or default) locale, and +www.example.es+ to load Spanish locale. Thus the _top-level domain name_ is used for locale setting. This has several advantages:
+One option you have is to set the locale from the domain name where your application runs. For example, we want +www.example.com+ to load the English (or default) locale, and +www.example.es+ to load the Spanish locale. Thus the _top-level domain name_ is used for locale setting. This has several advantages:
-* Locale is an _obvious_ part of the URL
+* The locale is an _obvious_ part of the URL
* People intuitively grasp in which language the content will be displayed
* It is very trivial to implement in Rails
* Search engines seem to like that content in different languages lives at different, inter-linked domains
@@ -208,7 +209,7 @@ def extract_locale_from_tld
end
</ruby>
-We can also set the locale from the _subdomain_ in very similar way:
+We can also set the locale from the _subdomain_ in a very similar way:
<ruby>
# Get locale code from request subdomain (like http://it.application.local:3000)
@@ -231,15 +232,15 @@ assuming you would set +APP_CONFIG[:deutsch_website_url]+ to some value like +ht
This solution has aforementioned advantages, however, you may not be able or may not want to provide different localizations ("language versions") on different domains. The most obvious solution would be to include locale code in the URL params (or request path).
-h4. Setting locale from the URL params
+h4. Setting the Locale from the URL Params
-Most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the +I18n.locale = params[:locale]+ _before_filter_ in the first example. We would like to have URLs like +www.example.com/books?locale=ja+ or +www.example.com/ja/books+ in this case.
+The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the +I18n.locale = params[:locale]+ _before_filter_ in the first example. We would like to have URLs like +www.example.com/books?locale=ja+ or +www.example.com/ja/books+ in this case.
-This approach has almost the same set of advantages as setting the locale from domain name: namely that it's RESTful and in accord with rest of the World Wide Web. It does require a little bit more work to implement, though.
+This approach has almost the same set of advantages as setting the locale from the domain name: namely that it's RESTful and in accord with the rest of the World Wide Web. It does require a little bit more work to implement, though.
-Getting the locale from +params+ and setting it accordingly is not hard; including it in every URL and thus *passing it through the requests* is. To include an explicit option in every URL (eg. +link_to( books_url(:locale => I18n.locale) )+) would be tedious and probably impossible, of course.
+Getting the locale from +params+ and setting it accordingly is not hard; including it in every URL and thus *passing it through the requests* is. To include an explicit option in every URL (e.g. +link_to( books_url(:locale => I18n.locale))+) would be tedious and probably impossible, of course.
-Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its "+*ApplicationController#default_url_options*+":http://api.rubyonrails.org/classes/ActionController/Base.html#M000515, which is useful precisely in this scenario: it enables us to set "defaults" for "+url_for+":http://api.rubyonrails.org/classes/ActionController/Base.html#M000503 and helper methods dependent on it (by implementing/overriding this method).
+Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its "+ApplicationController#default_url_options+":http://api.rubyonrails.org/classes/ActionController/Base.html#M000515, which is useful precisely in this scenario: it enables us to set "defaults" for "+url_for+":http://api.rubyonrails.org/classes/ActionController/Base.html#M000503 and helper methods dependent on it (by implementing/overriding this method).
We can include something like this in our ApplicationController then:
@@ -251,20 +252,20 @@ def default_url_options(options={})
end
</ruby>
-Every helper method dependent on +url_for+ (eg. helpers for named routes like +root_path+ or +root_url+, resource routes like +books_path+ or +books_url+, etc.) will now *automatically include the locale in the query string*, like this: +http://localhost:3001/?locale=ja+.
+Every helper method dependent on +url_for+ (e.g. helpers for named routes like +root_path+ or +root_url+, resource routes like +books_path+ or +books_url+, etc.) will now *automatically include the locale in the query string*, like this: +http://localhost:3001/?locale=ja+.
-You may be satisfied with this. It does impact the readability of URLs, though, when the locale "hangs" at the end of every URL in your application. Moreover, from the architectural standpoint, locale is usually hierarchically above the other parts of application domain: and URLs should reflect this.
+You may be satisfied with this. It does impact the readability of URLs, though, when the locale "hangs" at the end of every URL in your application. Moreover, from the architectural standpoint, locale is usually hierarchically above the other parts of the application domain: and URLs should reflect this.
-You probably want URLs look like this: +www.example.com/en/books+ (which loads English locale) and +www.example.com/nl/books+ (which loads Netherlands locale). This is achievable with the "over-riding +default_url_options+" strategy from above: you just have to set up your routes with "+path_prefix+":http://api.rubyonrails.org/classes/ActionController/Resources.html#M000354 option in this way:
+You probably want URLs to look like this: +www.example.com/en/books+ (which loads the English locale) and +www.example.com/nl/books+ (which loads the Netherlands locale). This is achievable with the "over-riding +default_url_options+" strategy from above: you just have to set up your routes with "+path_prefix+":http://api.rubyonrails.org/classes/ActionController/Resources.html#M000354 option in this way:
<ruby>
# config/routes.rb
map.resources :books, :path_prefix => '/:locale'
</ruby>
-Now, when you call +books_path+ method you should get +"/en/books"+ (for the default locale). An URL like +http://localhost:3001/nl/books+ should load the Netherlands locale, then, and following calls to +books_path+ should return +"/nl/books"+ (because the locale changed).
+Now, when you call the +books_path+ method you should get +"/en/books"+ (for the default locale). An URL like +http://localhost:3001/nl/books+ should load the Netherlands locale, then, and following calls to +books_path+ should return +"/nl/books"+ (because the locale changed).
-Of course, you need to take special care of root URL (usually "homepage" or "dashboard") of your application. An URL like +http://localhost:3001/nl+ will not work automatically, because the +map.root :controller => "dashboard"+ declaration in your +routes.rb+ doesn't take locale into account. (And rightly so. There's only one "root" URL.)
+Of course, you need to take special care of the root URL (usually "homepage" or "dashboard") of your application. An URL like +http://localhost:3001/nl+ will not work automatically, because the +map.root :controller => "dashboard"+ declaration in your +routes.rb+ doesn't take locale into account. (And rightly so: there's only one "root" URL.)
You would probably need to map URLs like these:
@@ -275,18 +276,18 @@ map.dashboard '/:locale', :controller => "dashboard"
Do take special care about the *order of your routes*, so this route declaration does not "eat" other ones. (You may want to add it directly before the +map.root+ declaration.)
-IMPORTANT: This solution has currently one rather big *downside*. Due to the _default_url_options_ implementation, you have to pass the +:id+ option explicitely, like this: +link_to 'Show', book_url(:id => book)+ and not depend on Rails' magic in code like +link_to 'Show', book+. If this should be a problem, have a look on two plugins which simplify working with routes in this way: Sven Fuchs's "_routing_filter_":http://github.com/svenfuchs/routing-filter/tree/master and Raul Murciano's "_translate_routes_":http://github.com/raul/translate_routes/tree/master. See also the page "How to encode the current locale in the URL":http://rails-i18n.org/wiki/pages/how-to-encode-the-current-locale-in-the-url in the Rails i18n Wiki.
+IMPORTANT: This solution has currently one rather big *downside*. Due to the _default_url_options_ implementation, you have to pass the +:id+ option explicitely, like this: +link_to 'Show', book_url(:id => book)+ and not depend on Rails' magic in code like +link_to 'Show', book+. If this should be a problem, have a look at two plugins which simplify work with routes in this way: Sven Fuchs's "routing_filter":http://github.com/svenfuchs/routing-filter/tree/master and Raul Murciano's "translate_routes":http://github.com/raul/translate_routes/tree/master. See also the page "How to encode the current locale in the URL":http://rails-i18n.org/wiki/pages/how-to-encode-the-current-locale-in-the-url in the Rails i18n Wiki.
-h4. Setting locale from the client supplied information
+h4. Setting the Locale from the Client Supplied Information
-In specific cases, it would make sense to set locale from client supplied information, ie. not from URL. This information may come for example from users' preffered language (set in their browser), can be based on users' geographical location inferred from their IP, or users can provide it simply by choosing locale in your application interface and saving it to their profile. This approach is more suitable for web-based applications or services, not for websites -- see the box about _sessions_, _cookies_ and RESTful architecture above.
+In specific cases, it would make sense to set the locale from client-supplied information, i.e. not from the URL. This information may come for example from the users' prefered language (set in their browser), can be based on the users' geographical location inferred from their IP, or users can provide it simply by choosing the locale in your application interface and saving it to their profile. This approach is more suitable for web-based applications or services, not for websites -- see the box about _sessions_, _cookies_ and RESTful architecture above.
-h5. Using Accept-Language
+h5. Using +Accept-Language+
One source of client supplied information would be an +Accept-Language+ HTTP header. People may "set this in their browser":http://www.w3.org/International/questions/qa-lang-priorities or other clients (such as _curl_).
-A trivial implementation of using +Accept-Language+ header would be:
+A trivial implementation of using an +Accept-Language+ header would be:
<ruby>
def set_locale
@@ -300,21 +301,21 @@ def extract_locale_from_accept_language_header
end
</ruby>
-Of course, in production environment you would need much robust code, and could use a plugin such as Iaian Hecker's "http_accept_language":http://github.com/iain/http_accept_language or even Rack middleware such as Ryan Tomayko's "locale":http://github.com/rtomayko/rack-contrib/blob/master/lib/rack/locale.rb.
+Of course, in a production environment you would need much more robust code, and could use a plugin such as Iain Hecker's "http_accept_language":http://github.com/iain/http_accept_language/tree/master or even Rack middleware such as Ryan Tomayko's "locale":http://github.com/rtomayko/rack-contrib/blob/master/lib/rack/locale.rb.
-h5. Using GeoIP (or similar) database
+h5. Using GeoIP (or Similar) Database
-Another way of choosing the locale from client's information would be to use a database for mapping client IP to region, such as "GeoIP Lite Country":http://www.maxmind.com/app/geolitecountry. The mechanics of the code would be very similar to the code above -- you would need to query database for user's IP, and lookup your preffered locale for the country/region/city returned.
+Another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as "GeoIP Lite Country":http://www.maxmind.com/app/geolitecountry. The mechanics of the code would be very similar to the code above -- you would need to query the database for the user's IP, and look up your prefered locale for the country/region/city returned.
-h5. User profile
+h5. User Profile
-You can also provide users of your application with means to set (and possibly over-ride) locale in your application interface, as well. Again, mechanics for this approach would be very similar to the code above -- you'd probably let users choose a locale from a dropdown list and save it to their profile in database. Then you'd set the locale to this value.
+You can also provide users of your application with means to set (and possibly over-ride) the locale in your application interface, as well. Again, mechanics for this approach would be very similar to the code above -- you'd probably let users choose a locale from a dropdown list and save it to their profile in the database. Then you'd set the locale to this value.
-h3. Internationalizing your application
+h3. Internationalizing your Application
-OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale should be used and how to preserve it between requests. With that in place, you're now ready for the really interesting stuff.
+OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests. With that in place, you're now ready for the really interesting stuff.
-Let's _internationalize_ our application, ie. abstract every locale-specific parts, and that _localize_ it, ie. provide neccessary translations for these abstracts.
+Let's _internationalize_ our application, i.e. abstract every locale-specific parts, and then _localize_ it, i.e. provide neccessary translations for these abstracts.
You most probably have something like this in one of your applications:
@@ -359,7 +360,7 @@ When you now render this view, it will show an error message which tells you tha
!images/i18n/demo_translation_missing.png(rails i18n demo translation missing)!
-NOTE: Rails adds a +t+ (+translate+) helper method to your views so that you do not need to spell out +I18n.t+ all the time. Additionally this helper will catch missing translations and wrap the resulting error message into a +<span class="translation_missing">+.
+NOTE: Rails adds a +t+ (+translate+) helper method to your views so that you do not need to spell out +I18n.t+ all the time. Additionally this helper will catch missing translations and wrap the resulting error message into a +&lt;span class="translation_missing"&gt;+.
So let's add the missing translations into the dictionary files (i.e. do the "localization" part):
@@ -385,9 +386,9 @@ And when you change the URL to pass the pirate locale (+http://localhost:3000?lo
NOTE: You need to restart the server when you add new locale files.
-You may use YAML (+.yml+) or plain Ruby (+.rb+) files for storing your translations in SimpleStore. YAML is the preffered option among Rails developers, has one big disadvantage, though. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you may easily find what's wrong. (If you encounter any "weird issues" with YAML dictionaries, try putting the relevant portion of your dictionary into Ruby file.)
+You may use YAML (+.yml+) or plain Ruby (+.rb+) files for storing your translations in SimpleStore. YAML is the prefered option among Rails developers. However, it has one big disadvantage. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you may easily find what's wrong. (If you encounter any "weird issues" with YAML dictionaries, try putting the relevant portion of your dictionary into a Ruby file.)
-h4. Adding Date/Time formats
+h4. Adding Date/Time Formats
OK! Now let's add a timestamp to the view, so we can demo the *date/time localization* feature as well. To localize the time format you pass the Time object to +I18n.l+ or (preferably) use Rails' +#l+ helper. You can pick a format by passing the +:format+ option -- by default the +:default+ format is used.
@@ -412,17 +413,17 @@ So that would give you:
!images/i18n/demo_localized_pirate.png(rails i18n demo localized time to pirate)!
-TIP: Right now you might need to add some more date/time formats in order to make the I18n backend work as expected. Of course, there's a great chance that somebody already did all the work by *translating Rails's defaults for your locale*. See the "rails-i18n repository at Github":http://github.com/svenfuchs/rails-i18n/tree/master/rails/locale for an archive of various locale files. When you put such file(s) in +config/locale/+ directory, they will automatically ready for use.
+TIP: Right now you might need to add some more date/time formats in order to make the I18n backend work as expected (at least for the 'pirate' locale). Of course, there's a great chance that somebody already did all the work by *translating Rails's defaults for your locale*. See the "rails-i18n repository at Github":http://github.com/svenfuchs/rails-i18n/tree/master/rails/locale for an archive of various locale files. When you put such file(s) in +config/locale/+ directory, they will automatically be ready for use.
-h4. Localized views
+h4. Localized Views
-Rails 2.3 brings one convenient feature: localized views (templates). Let's say you have a _BooksController_ in your application. Your _index_ action renders content in +app/views/books/index.html.erb+ template. When you put a _localized variant_ of this template: *+index.es.html.erb+* in the same directory, Rails will render content in this template, when the locale is set to +:es+. When the locale is set to the default locale, generic +index.html.erb+ view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in +public+, etc.)
+Rails 2.3 introduces another convenient localization feature: localized views (templates). Let's say you have a _BooksController_ in your application. Your _index_ action renders content in +app/views/books/index.html.erb+ template. When you put a _localized variant_ of this template: *+index.es.html.erb+* in the same directory, Rails will render content in this template, when the locale is set to +:es+. When the locale is set to the default locale, the generic +index.html.erb+ view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in +public+, etc.)
-You can make use this feature eg. when working with great amount of static content, which would be clumsy to put inside YAML or Ruby dictionaries. Bear in mind, though, that any change you would like to do later to the template must be propagated to all of them.
+You can make use of this feature, e.g. when working with a large amount of static content, which would be clumsy to put inside YAML or Ruby dictionaries. Bear in mind, though, that any change you would like to do later to the template must be propagated to all of them.
-h4. Organization of locale files
+h4. Organization of Locale Files
-When you are using the default SimpleStore, shipped with the i18n library, dictionaries are stored in plain-text files on the disc. Putting translations for all parts of your application in one file per locale could be hard to manage. You can store these files in a hierarchy which makes sense to you.
+When you are using the default SimpleStore shipped with the i18n library, dictionaries are stored in plain-text files on the disc. Putting translations for all parts of your application in one file per locale could be hard to manage. You can store these files in a hierarchy which makes sense to you.
For example, your +config/locale+ directory could look like this:
@@ -449,9 +450,9 @@ For example, your +config/locale+ directory could look like this:
|-----en.rb
</pre>
-This way, you can separate model and model attribute names from text inside views, and all of this from the "defaults" (eg. date and time formats). Other stores for the i18n library could provide different means of such separation.
+This way, you can separate model and model attribute names from text inside views, and all of this from the "defaults" (e.g. date and time formats). Other stores for the i18n library could provide different means of such separation.
-NOTE: The default locale loading mechanism in Rails does not load locale files in nested dictionaries, like we have here. So, for this to work, we must explicitely tell Rails to look further:
+NOTE: The default locale loading mechanism in Rails does not load locale files in nested dictionaries, like we have here. So, for this to work, we must explicitly tell Rails to look further:
<ruby>
# config/environment.rb
@@ -460,7 +461,7 @@ NOTE: The default locale loading mechanism in Rails does not load locale files i
Do check the "Rails i18n Wiki":http://rails-i18n.org/wiki for list of tools available for managing translations.
-h3. Overview of the I18n API features
+h3. Overview of the I18n API Features
You should have good understanding of using the i18n library now, knowing all neccessary aspects of internationalizing a basic Rails application. In the following chapters, we'll cover it's features in more depth.
@@ -469,11 +470,11 @@ Covered are features like these:
* looking up translations
* interpolating data into translations
* pluralizing translations
-* localizing dates, numbers, currency etc.
+* localizing dates, numbers, currency, etc.
-h4. Looking up translations
+h4. Looking up Translations
-h5. Basic lookup, scopes and nested keys
+h5. Basic Lookup, Scopes and Nested Keys
Translations are looked up by keys which can be both Symbols or Strings, so these calls are equivalent:
@@ -482,66 +483,66 @@ I18n.t :message
I18n.t 'message'
</ruby>
-+translate+ also takes a +:scope+ option which can contain one or many additional keys that will be used to specify a “namespace” or scope for a translation key:
+The +translate+ method also takes a +:scope+ option which can contain one or more additional keys that will be used to specify a “namespace” or scope for a translation key:
<ruby>
-I18n.t :invalid, :scope => [:active_record, :error_messages]
+I18n.t :invalid, :scope => [:activerecord, :errors, :messages]
</ruby>
This looks up the +:invalid+ message in the Active Record error messages.
-Additionally, both the key and scopes can be specified as dot separated keys as in:
+Additionally, both the key and scopes can be specified as dot-separated keys as in:
<ruby>
-I18n.translate :"active_record.error_messages.invalid"
+I18n.translate :"activerecord.errors.messages.invalid"
</ruby>
Thus the following calls are equivalent:
<ruby>
-I18n.t 'active_record.error_messages.invalid'
-I18n.t 'error_messages.invalid', :scope => :active_record
-I18n.t :invalid, :scope => 'active_record.error_messages'
-I18n.t :invalid, :scope => [:active_record, :error_messages]
+I18n.t 'activerecord.errors.messages.invalid'
+I18n.t 'errors.messages.invalid', :scope => :active_record
+I18n.t :invalid, :scope => 'activerecord.errors.messages'
+I18n.t :invalid, :scope => [:activerecord, :errors, :messages]
</ruby>
h5. Defaults
-When a default option is given its value will be returned if the translation is missing:
+When a +:default+ option is given, its value will be returned if the translation is missing:
<ruby>
I18n.t :missing, :default => 'Not here'
# => 'Not here'
</ruby>
-If the default value is a Symbol it will be used as a key and translated. One can provide multiple values as default. The first one that results in a value will be returned.
+If the +:default+ value is a Symbol, it will be used as a key and translated. One can provide multiple values as default. The first one that results in a value will be returned.
-E.g. the following first tries to translate the key +:missing+ and then the key +:also_missing.+ As both do not yield a result the string "Not here" will be returned:
+E.g., the following first tries to translate the key +:missing+ and then the key +:also_missing.+ As both do not yield a result, the string "Not here" will be returned:
<ruby>
I18n.t :missing, :default => [:also_missing, 'Not here']
# => 'Not here'
</ruby>
-h5. Bulk and namespace lookup
+h5. Bulk and Namespace Lookup
-To lookup multiple translations at once an array of keys can be passed:
+To look up multiple translations at once, an array of keys can be passed:
<ruby>
-I18n.t [:odd, :even], :scope => 'active_record.error_messages'
+I18n.t [:odd, :even], :scope => 'activerecord.errors.messages'
# => ["must be odd", "must be even"]
</ruby>
-Also, a key can translate to a (potentially nested) hash as grouped translations. E.g. one can receive all Active Record error messages as a Hash with:
+Also, a key can translate to a (potentially nested) hash of grouped translations. E.g., one can receive _all_ Active Record error messages as a Hash with:
<ruby>
-I18n.t 'active_record.error_messages'
+I18n.t 'activerecord.errors.messages'
# => { :inclusion => "is not included in the list", :exclusion => ... }
</ruby>
-h5. "Lazy" lookup
+h5. "Lazy" Lookup
-Rails 2.3 implements convenient way to lookup locale inside _views_. When you have following dictionary:
+Rails 2.3 implements a convenient way to look up the locale inside _views_. When you have the following dictionary:
<yaml>
es:
@@ -550,7 +551,7 @@ es:
title: "Título"
</yaml>
-you can lookup the +books.index.title+ value *inside* +app/views/books/index.html.erb+ template like this (note the dot):
+you can look up the +books.index.title+ value *inside* +app/views/books/index.html.erb+ template like this (note the dot):
<ruby>
<%= t '.title' %>
@@ -568,12 +569,11 @@ I18n.translate :thanks, :name => 'Jeremy'
# => 'Thanks Jeremy!'
</ruby>
-If a translation uses +:default+ or +:scope+ as a interpolation variable an I+18n::ReservedInterpolationKey+ exception is raised. If a translation expects an interpolation variable but it has not been passed to +#translate+ an +I18n::MissingInterpolationArgument+ exception is raised.
-
+If a translation uses +:default+ or +:scope+ as an interpolation variable, an I+18n::ReservedInterpolationKey+ exception is raised. If a translation expects an interpolation variable, but this has not been passed to +#translate+, an +I18n::MissingInterpolationArgument+ exception is raised.
h4. Pluralization
-In English there's only a singular and a plural form for a given string, e.g. "1 message" and "2 messages". Other languages ("Arabic":http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html#ar, "Japanese":http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html#ja, "Russian":http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html#ru and many more) have different grammars that have additional or less "plural forms":http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html. Thus, the I18n API provides a flexible pluralization feature.
+In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ("Arabic":http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html#ar, "Japanese":http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html#ja, "Russian":http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html#ru and many more) have different grammars that have additional or fewer "plural forms":http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html. Thus, the I18n API provides a flexible pluralization feature.
The +:count+ interpolation variable has a special role in that it both is interpolated to the translation and used to pick a pluralization from the translations according to the pluralization rules defined by CLDR:
@@ -594,13 +594,13 @@ entry[count == 1 ? 0 : 1]
I.e. the translation denoted as +:one+ is regarded as singular, the other is used as plural (including the count being zero).
-If the lookup for the key does not return an Hash suitable for pluralization an +18n::InvalidPluralizationData+ exception is raised.
+If the lookup for the key does not return a Hash suitable for pluralization, an +18n::InvalidPluralizationData+ exception is raised.
-h4. Setting and passing a locale
+h4. Setting and Passing a Locale
The locale can be either set pseudo-globally to +I18n.locale+ (which uses +Thread.current+ like, e.g., +Time.zone+) or can be passed as an option to +#translate+ and +#localize+.
-If no locale is passed +I18n.locale+ is used:
+If no locale is passed, +I18n.locale+ is used:
<ruby>
I18n.locale = :de
@@ -615,15 +615,15 @@ I18n.t :foo, :locale => :de
I18n.l Time.now, :locale => :de
</ruby>
-+I18n.locale+ defaults to +I18n.default_locale+ which defaults to :+en+. The default locale can be set like this:
+The +I18n.locale+ defaults to +I18n.default_locale+ which defaults to :+en+. The default locale can be set like this:
<ruby>
I18n.default_locale = :de
</ruby>
-h3. How to store your custom translations
+h3. How to Store your Custom Translations
-The shipped Simple backend allows you to store translations in both plain Ruby and YAML format. [2]
+The Simple backend shipped with Active Support allows you to store translations in both plain Ruby and YAML format. [2]
For example a Ruby Hash providing translations can look like this:
@@ -645,9 +645,9 @@ pt:
bar: baz
</ruby>
-As you see in both cases the toplevel key is the locale. +:foo+ is a namespace key and +:bar+ is the key for the translation "baz".
+As you see, in both cases the toplevel key is the locale. +:foo+ is a namespace key and +:bar+ is the key for the translation "baz".
-Here is a "real" example from the ActiveSupport +en.yml+ translations YAML file:
+Here is a "real" example from the Active Support +en.yml+ translations YAML file:
<ruby>
en:
@@ -667,11 +667,11 @@ I18n.t :short, :scope => 'date.formats'
I18n.t :short, :scope => [:date, :formats]
</ruby>
-Generally we recommend using YAML as a format for storing translations. There are cases though where you want to store Ruby lambdas as part of your locale data, e.g. for special date.
+Generally we recommend using YAML as a format for storing translations. There are cases, though, where you want to store Ruby lambdas as part of your locale data, e.g. for special date formats.
-h4. Translations for Active Record models
+h4. Translations for Active Record Models
-You can use the methods +Model.human_name+ and +Model.human_attribute_name(attribute)+ to transparently lookup translations for your model and attribute names.
+You can use the methods +Model.human_name+ and +Model.human_attribute_name(attribute)+ to transparently look up translations for your model and attribute names.
For example when you add the following translations:
@@ -688,9 +688,9 @@ en:
Then +User.human_name+ will return "Dude" and +User.human_attribute_name(:login)+ will return "Handle".
-h5. Error message scopes
+h5. Error Message Scopes
-Active Record validation error messages can also be translated easily. Active Record gives you a couple of namespaces where you can place your message translations in order to provide different messages and translation for certain models, attributes and/or validations. It also transparently takes single table inheritance into account.
+Active Record validation error messages can also be translated easily. Active Record gives you a couple of namespaces where you can place your message translations in order to provide different messages and translation for certain models, attributes, and/or validations. It also transparently takes single table inheritance into account.
This gives you quite powerful means to flexibly adjust your messages to your application's needs.
@@ -702,7 +702,7 @@ class User < ActiveRecord::Base
end
</ruby>
-The key for the error message in this case is +:blank+. Active Record will lookup this key in the namespaces:
+The key for the error message in this case is +:blank+. Active Record will look up this key in the namespaces:
<ruby>
activerecord.errors.models.[model_name].attributes.[attribute_name]
@@ -738,9 +738,9 @@ activerecord.errors.models.user.blank
activerecord.errors.messages.blank
</ruby>
-This way you can provide special translations for various error messages at different points in your models inheritance chain and in the attributes, models or default scopes.
+This way you can provide special translations for various error messages at different points in your models inheritance chain and in the attributes, models, or default scopes.
-h5. Error message interpolation
+h5. Error Message Interpolation
The translated model name, translated attribute name, and value are always available for interpolation.
@@ -771,9 +771,9 @@ So, for example, instead of the default error message +"can not be blank"+ you c
| validates_numericality_of | :odd | :odd | -|
| validates_numericality_of | :even | :even | -|
-h5. Translations for the Active Record error_messages_for helper
+h5. Translations for the Active Record +error_messages_for+ Helper
-If you are using the Active Record +error_messages_for+ helper you will want to add translations for it.
+If you are using the Active Record +error_messages_for+ helper, you will want to add translations for it.
Rails ships with the following translations:
@@ -788,44 +788,44 @@ en:
body: "There were problems with the following fields:"
</yaml>
-h4. Overview of other built-in methods that provide I18n support
+h4. Overview of Other Built-In Methods that Provide I18n Support
Rails uses fixed strings and other localizations, such as format strings and other format information in a couple of helpers. Here's a brief overview.
-h5. ActionView helper methods
+h5. Action View Helper Methods
-* +distance_of_time_in_words+ translates and pluralizes its result and interpolates the number of seconds, minutes, hours and so on. See "datetime.distance_in_words":http://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L51 translations.
+* +distance_of_time_in_words+ translates and pluralizes its result and interpolates the number of seconds, minutes, hours, and so on. See "datetime.distance_in_words":http://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L51 translations.
-* +datetime_select+ and +select_month+ use translated month names for populating the resulting select tag. See "date.month_names":http://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L15 for translations. +datetime_select+ also looks up the order option from "date.order":http://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L18 (unless you pass the option explicitely). All date select helpers translate the prompt using the translations in the "datetime.prompts":http://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L83 scope if applicable.
+* +datetime_select+ and +select_month+ use translated month names for populating the resulting select tag. See "date.month_names":http://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L15 for translations. +datetime_select+ also looks up the order option from "date.order":http://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L18 (unless you pass the option explicitely). All date selection helpers translate the prompt using the translations in the "datetime.prompts":http://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L83 scope if applicable.
-* The +number_to_currency+, +number_with_precision+, +number_to_percentage+, +number_with_delimiter+ and +humber_to_human_size+ helpers use the number format settings located in the "number":http://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L2 scope.
+* The +number_to_currency+, +number_with_precision+, +number_to_percentage+, +number_with_delimiter+, and +number_to_human_size+ helpers use the number format settings located in the "number":http://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L2 scope.
-h5. Active Record methods
+h5. Active Record Methods
* +human_name+ and +human_attribute_name+ use translations for model names and attribute names if available in the "activerecord.models":http://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml#L43 scope. They also support translations for inherited class names (e.g. for use with STI) as explained above in "Error message scopes".
* +ActiveRecord::Errors#generate_message+ (which is used by Active Record validations but may also be used manually) uses +human_name+ and +human_attribute_name+ (see above). It also translates the error message and supports translations for inherited class names as explained above in "Error message scopes".
-*+ ActiveRecord::Errors#full_messages+ prepends the attribute name to the error message using a separator that will be looked up from "activerecord.errors.format.separator":http://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L91 (and defaults to +' '+).
+*+ ActiveRecord::Errors#full_messages+ prepends the attribute name to the error message using a separator that will be looked up from "activerecord.errors.format.separator":http://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L91 (and which defaults to +'&nbsp;'+).
-h5. ActiveSupport methods
+h5. Active Support Methods
* +Array#to_sentence+ uses format settings as given in the "support.array":http://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L30 scope.
-h3. Customize your I18n setup
+h3. Customize your I18n Setup
-h4. Using different backends
+h4. Using Different Backends
-For several reasons the shipped Simple backend only does the "simplest thing that ever could work" _for Ruby on Rails_ [3] ... which means that it is only guaranteed to work for English and, as a side effect, languages that are very similar to English. Also, the simple backend is only capable of reading translations but can not dynamically store them to any format.
+For several reasons the Simple backend shipped with Active Support only does the "simplest thing that could possibly work" _for Ruby on Rails_ [3] ... which means that it is only guaranteed to work for English and, as a side effect, languages that are very similar to English. Also, the simple backend is only capable of reading translations but can not dynamically store them to any format.
-That does not mean you're stuck with these limitations though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs. E.g. you could exchange it with Globalize's Static backend:
+That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs. E.g. you could exchange it with Globalize's Static backend:
<ruby>
I18n.backend = Globalize::Backend::Static.new
</ruby>
-h4. Using different exception handlers
+h4. Using Different Exception Handlers
The I18n API defines the following exceptions that will be raised by backends when the corresponding unexpected conditions occur:
@@ -838,11 +838,11 @@ ReservedInterpolationKey # the translation contains a reserved interpolation
UnknownFileType # the backend does not know how to handle a file type that was added to I18n.load_path
</ruby>
-The I18n API will catch all of these exceptions when they were thrown in the backend and pass them to the default_exception_handler method. This method will re-raise all exceptions except for +MissingTranslationData+ exceptions. When a +MissingTranslationData+ exception has been caught it will return the exception’s error message string containing the missing key/scope.
+The I18n API will catch all of these exceptions when they are thrown in the backend and pass them to the default_exception_handler method. This method will re-raise all exceptions except for +MissingTranslationData+ exceptions. When a +MissingTranslationData+ exception has been caught, it will return the exception’s error message string containing the missing key/scope.
The reason for this is that during development you'd usually want your views to still render even though a translation is missing.
-In other contexts you might want to change this behaviour though. E.g. the default exception handling does not allow to catch missing translations during automated tests easily. For this purpose a different exception handler can be specified. The specified exception handler must be a method on the I18n module:
+In other contexts you might want to change this behaviour, though. E.g. the default exception handling does not allow to catch missing translations during automated tests easily. For this purpose a different exception handler can be specified. The specified exception handler must be a method on the I18n module:
<ruby>
module I18n
@@ -856,9 +856,9 @@ I18n.exception_handler = :just_raise_that_exception
This would re-raise all caught exceptions including +MissingTranslationData+.
-Another example where the default behaviour is less desirable is the Rails TranslationHelper which provides the method +#t+ (as well as +#translate+). When a +MissingTranslationData+ exception occurs in this context the helper wraps the message into a span with the CSS class +translation_missing+.
+Another example where the default behaviour is less desirable is the Rails TranslationHelper which provides the method +#t+ (as well as +#translate+). When a +MissingTranslationData+ exception occurs in this context, the helper wraps the message into a span with the CSS class +translation_missing+.
-To do so the helper forces +I18n#translate+ to raise exceptions no matter what exception handler is defined by setting the +:raise+ option:
+To do so, the helper forces +I18n#translate+ to raise exceptions no matter what exception handler is defined by setting the +:raise+ option:
<ruby>
I18n.t :foo, :raise => true # always re-raises exceptions from the backend
@@ -866,16 +866,16 @@ I18n.t :foo, :raise => true # always re-raises exceptions from the backend
h3. Conclusion
-At this point you hopefully have a good overview about how I18n support in Ruby on Rails works and are ready to start translating your project.
+At this point you should have a good overview about how I18n support in Ruby on Rails works and are ready to start translating your project.
If you find anything missing or wrong in this guide please file a ticket on "our issue tracker":http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview. If you want to discuss certain portions or have questions please sign up to our "mailinglist":http://groups.google.com/group/rails-i18n.
h3. Contributing to Rails I18n
-I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in plugins and real applications first and then cherry-picking the best bread of most widely useful features second for inclusion to the core.
+I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in plugins and real applications first, and only then cherry-picking the best-of-bread of most widely useful features for inclusion in the core.
-Thus we encourage everybody to experiment with new ideas and features in plugins or other libraries and make them available to the community. (Don't forget to announce your work on our "mailinglist":http://groups.google.com/group/rails-i18n!)
+Thus we encourage everybody to experiment with new ideas and features in plugins or other libraries and make them available to the community. (Don't forget to announce your work on our "mailing list":http://groups.google.com/group/rails-i18n!)
If you find your own locale (language) missing from our "example translations data":http://github.com/svenfuchs/rails-i18n/tree/master/rails/locale repository for Ruby on Rails, please "_fork_":http://github.com/guides/fork-a-project-and-submit-your-modifications the repository, add your data and send a "pull request":http://github.com/guides/pull-requests.
@@ -904,7 +904,7 @@ fn1. Or, to quote "Wikipedia":http://en.wikipedia.org/wiki/Internationalization_
fn2. Other backends might allow or require to use other formats, e.g. a GetText backend might allow to read GetText files.
-fn3. One of these reasons is that we don't want to any unnecessary load for applications that do not need any I18n capabilities, so we need to keep the I18n library as simple as possible for English. Another reason is that it is virtually impossible to implement a one-fits-all solution for all problems related to I18n for all existing languages. So a solution that allows us to exchange the entire implementation easily is appropriate anyway. This also makes it much easier to experiment with custom features and extensions.
+fn3. One of these reasons is that we don't want to imply any unnecessary load for applications that do not need any I18n capabilities, so we need to keep the I18n library as simple as possible for English. Another reason is that it is virtually impossible to implement a one-fits-all solution for all problems related to I18n for all existing languages. So a solution that allows us to exchange the entire implementation easily is appropriate anyway. This also makes it much easier to experiment with custom features and extensions.
h3. Changelog
diff --git a/railties/guides/source/index.erb.textile b/railties/guides/source/index.erb.textile
index 3b36ada8c2..4c8dd65a04 100644
--- a/railties/guides/source/index.erb.textile
+++ b/railties/guides/source/index.erb.textile
@@ -32,7 +32,7 @@ h3. Models
This guide covers how you can use Active Record migrations to alter your database in a structured and organized manner.
<% end %>
-<% guide("Active Record Validations and Callbacks", 'activerecord_validations_callbacks.html', :ticket => 26) do %>
+<% guide("Active Record Validations and Callbacks", 'activerecord_validations_callbacks.html') do %>
This guide covers how you can use Active Record validations and callbacks.
<% end %>
@@ -40,7 +40,7 @@ h3. Models
This guide covers all the associations provided by Active Record.
<% end %>
-<% guide("Active Record Query Interface", 'active_record_querying.html', :ticket => 16) do %>
+<% guide("Active Record Query Interface", 'active_record_querying.html') do %>
This guide covers the database query interface provided by Active Record.
<% end %>
</dl>
@@ -77,7 +77,7 @@ h3. Digging Deeper
This guide covers Rails integration with Rack and interfacing with other Rack components.
<% end %>
-<% guide("Rails Internationalization API", 'i18n.html', :ticket => 23) do %>
+<% guide("Rails Internationalization API", 'i18n.html') do %>
This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country and so on.
<% end %>
@@ -109,10 +109,6 @@ h3. Digging Deeper
This guide covers the basic configuration settings for a Rails application.
<% end %>
-<% guide("Rails on Rack", 'rails_on_rack.html', :ticket => 58) do %>
- This guide covers Rails integration with Rack and interfacing with other Rack components.
-<% end %>
-
<% guide("Rails Command Line Tools and Rake tasks", 'command_line.html', :ticket => 29) do %>
This guide covers the command line tools and rake tasks provided by Rails.
<% end %>
@@ -121,7 +117,7 @@ h3. Digging Deeper
Various caching techniques provided by Rails.
<% end %>
-<% guide("Contributing to Rails", 'contributing.html', :ticket => 64) do %>
+<% guide("Contributing to Rails", 'contributing_to_rails.html') do %>
Rails is not "somebody else's framework." This guide covers a variety of ways that you can get involved in the ongoing development of Rails.
<% end %>
diff --git a/railties/guides/source/layout.html.erb b/railties/guides/source/layout.html.erb
index b8dd4aa012..eb66366d07 100644
--- a/railties/guides/source/layout.html.erb
+++ b/railties/guides/source/layout.html.erb
@@ -16,15 +16,15 @@
<body class="guide">
<div id="topNav">
<div class="wrapper">
- <strong>More at <a href="http://www.rubyonrails.org/">rubyonrails.org:</a> </strong>
- <a href="http://www.rubyonrails.org/">Overview</a> |
- <a href="http://www.rubyonrails.org/download">Download</a> |
- <a href="http://www.rubyonrails.org/deploy">Deploy</a> |
+ <strong>More at <a href="http://rubyonrails.org/">rubyonrails.org:</a> </strong>
+ <a href="http://rubyonrails.org/">Overview</a> |
+ <a href="http://rubyonrails.org/download">Download</a> |
+ <a href="http://rubyonrails.org/deploy">Deploy</a> |
<a href="http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/overview">Code</a> |
- <a href="http://www.rubyonrails.org/screencasts">Screencasts</a> |
- <a href="http://www.rubyonrails.org/documentation">Documentation</a> |
- <a href="http://www.rubyonrails.org/ecosystem">Ecosystem</a> |
- <a href="http://www.rubyonrails.org/community">Community</a> |
+ <a href="http://rubyonrails.org/screencasts">Screencasts</a> |
+ <a href="http://rubyonrails.org/documentation">Documentation</a> |
+ <a href="http://rubyonrails.org/ecosystem">Ecosystem</a> |
+ <a href="http://rubyonrails.org/community">Community</a> |
<a href="http://weblog.rubyonrails.org/">Blog</a>
</div>
</div>
@@ -65,7 +65,7 @@
<dd><a href="rails_on_rack.html">Rails on Rack</a></dd>
<dd><a href="command_line.html">Rails Command Line Tools and Rake Tasks</a></dd>
<dd><a href="caching_with_rails.html">Caching with Rails</a></dd>
- <dd><a href="contributing.html">Contributing to Rails</a></dd>
+ <dd><a href="contributing_to_rails.html">Contributing to Rails</a></dd>
</dl>
</div>
</li>
@@ -95,7 +95,7 @@
<hr class="hide" />
<div id="footer">
<div class="wrapper">
- <p>This work is licensed under a <a href="http://creativecommons.org/licenses/by-sa/3.0">Creative Commons Attribution-Share Alike 3.0</a> License</a></p>
+ <p>This work is licensed under a <a href="http://creativecommons.org/licenses/by-sa/3.0/">Creative Commons Attribution-Share Alike 3.0</a> License</a></p>
<p>"Rails", "Ruby on Rails", and the Rails logo are trademarks of David Heinemeier Hansson. All rights reserved.</p>
</div>
</div>
diff --git a/railties/guides/source/layouts_and_rendering.textile b/railties/guides/source/layouts_and_rendering.textile
index 5e2cedcf0c..809d2b2172 100644
--- a/railties/guides/source/layouts_and_rendering.textile
+++ b/railties/guides/source/layouts_and_rendering.textile
@@ -39,7 +39,7 @@ Rails will automatically render +app/views/books/show.html.erb+ after running th
NOTE: The actual rendering is done by subclasses of +ActionView::TemplateHandlers+. This guide does not dig into that process, but it's important to know that the file extension on your view controls the choice of template handler. In Rails 2, the standard extensions are +.erb+ for ERB (HTML with embedded Ruby), +.rjs+ for RJS (javascript with embedded ruby) and +.builder+ for Builder (XML generator). You'll also find +.rhtml+ used for ERB templates and +.rxml+ for Builder templates, but those extensions are now formally deprecated and will be removed from a future version of Rails.
-h4. Using render
+h4. Using +render+
In most cases, the +ActionController::Base#render+ method does the heavy lifting of rendering your application's content for use by a browser. There are a variety of ways to customize the behavior of +render+. You can render the default view for a Rails template, or a specific template, or a file, or inline code, or nothing at all. You can render text, JSON, or XML. You can specify the content type or HTTP status of the rendered response as well.
@@ -140,7 +140,7 @@ NOTE: By default, the file is rendered without using the current layout. If you
TIP: If you're running on Microsoft Windows, you should use the +:file+ option to render a file, because Windows filenames do not have the same format as Unix filenames.
-h5. Using render with :inline
+h5. Using +render+ with +:inline+
The +render+ method can do without a view completely, if you're willing to use the +:inline+ option to supply ERB as part of the method call. This is perfectly valid:
@@ -158,7 +158,7 @@ render :inline =>
"xml.p {'Horrid coding practice!'}", :type => :builder
</ruby>
-h5. Using render with :update
+h5. Using +render+ with +:update+
You can also render javascript-based page updates inline using the +:update+ option to +render+:
@@ -212,7 +212,7 @@ render :js => "alert('Hello Rails');"
This will send the supplied string to the browser with a MIME type of +text/javascript+.
-h5. Options for render
+h5. Options for +render+
Calls to the +render+ method generally accept four options:
@@ -221,7 +221,7 @@ Calls to the +render+ method generally accept four options:
* +:status+
* +:location+
-h6. The :content_type Option
+h6. The +:content_type+ Option
By default, Rails will serve the results of a rendering operation with the MIME content-type of +text/html+ (or +application/json+ if you use the +:json+ option, or +application/xml+ for the +:xml+ option.). There are times when you might like to change this, and you can do so by setting the +:content_type+ option:
@@ -229,7 +229,7 @@ By default, Rails will serve the results of a rendering operation with the MIME
render :file => filename, :content_type => 'application/rss'
</ruby>
-h6. The :layout Option
+h6. The +:layout+ Option
With most of the options to +render+, the rendered content is displayed as part of the current layout. You'll learn more about layouts and how to use them later in this guide.
@@ -245,7 +245,7 @@ You can also tell Rails to render with no layout at all:
render :layout => false
</ruby>
-h6. The :status Option
+h6. The +:status+ Option
Rails will automatically generate a response with the correct HTML status code (in most cases, this is +200 OK+). You can use the +:status+ option to change this:
@@ -256,7 +256,7 @@ render :status => :forbidden
Rails understands either numeric status codes or symbols for status codes. You can find its list of status codes in +actionpack/lib/action_controller/status_codes.rb+. You can also see there how Rails maps symbols to status codes.
-h6. The :location Option
+h6. The +:location+ Option
You can use the +:location+ option to set the HTTP +Location+ header:
@@ -316,7 +316,7 @@ Now, if the current user is a special user, they'll get a special layout when vi
<ruby>
class ProductsController < ApplicationController
- layout proc{ |controller| controller.
+ layout proc { |controller| controller.request.xhr? ? 'popup' : 'application' }
# ...
end
</ruby>
@@ -327,13 +327,12 @@ Layouts specified at the controller level support +:only+ and +:except+ options
<ruby>
class ProductsController < ApplicationController
- layout "inventory", :only => :index
layout "product", :except => [:index, :rss]
#...
end
</ruby>
-With those declarations, the +inventory+ layout would be used only for the +index+ method, the +product+ layout would be used for everything else except the +rss+ method, and the +rss+ method will have its layout determined by the automatic layout rules.
+With this declaration, the +product+ layout would be used for everything but the +rss+ and +index+ methods.
h6. Layout Inheritance
@@ -403,6 +402,7 @@ def show
if @book.special?
render :action => "special_show"
end
+ render :action => "regular_show"
end
</ruby>
@@ -414,10 +414,24 @@ def show
if @book.special?
render :action => "special_show" and return
end
+ render :action => "regular_show"
end
</ruby>
-h4. Using redirect_to
+Note that the implicit render done by ActionController detects if +render+ has been called, and thus avoids this error. So this code will work with problems:
+
+<ruby>
+ def show
+ @book = Book.find(params[:id])
+ if @book.special?
+ render :action => "special_show"
+ end
+ end
+</ruby>
+
+This will render a book with +special?+ set with the +special_show+ template, while other books will render with the default +show+ template.
+
+h4. Using +redirect_to+
Another way to handle returning responses to an HTTP request is with +redirect_to+. As you've seen, +render+ tells Rails which view (or other asset) to use in constructing a response. The +redirect_to+ method does something completely different: it tells the browser to send a new request for a different URL. For example, you could redirect from wherever you are in your code to the index of photos in your application with this call:
@@ -441,7 +455,7 @@ redirect_to photos_path, :status => 301
Just like the +:status+ option for +render+, +:status+ for +redirect_to+ accepts both numeric and symbolic header designations.
-h5. The Difference Between render and redirect
+h5. The Difference Between +render+ and +redirect_to+
Sometimes inexperienced developers conceive of +redirect_to+ as a sort of +goto+ command, moving execution from one place to another in your Rails code. This is _not_ correct. Your code stops running and waits for a new request for the browser. It just happens that you've told the browser what request it should make next, by sending back an HTTP 302 status code.
@@ -455,7 +469,7 @@ end
def show
@book = Book.find(params[:id])
if @book.nil?
- render :action => "index" and return
+ render :action => "index"
end
end
</ruby>
@@ -470,14 +484,14 @@ end
def show
@book = Book.find(params[:id])
if @book.nil?
- redirect_to :action => "index" and return
+ redirect_to :action => "index"
end
end
</ruby>
With this code, the browser will make a fresh request for the index page, the code in the +index+ method will run, and all will be well.
-h4. Using head To Build Header-Only Responses
+h4. Using +head+ To Build Header-Only Responses
The +head+ method exists to let you send back responses to the browser that have only headers. It provides a more obvious alternative to calling +render :nothing+. The +head+ method takes one response, which is interpreted as a hash of header names and values. For example, you can return only an error header:
@@ -514,7 +528,7 @@ You can use these tags in layouts or other views, although the tags other than +
WARNING: The asset tags do _not_ verify the existence of the assets at the specified locations; they simply assume that you know what you're doing and generate the link.
-h5. Linking to Feeds with auto_discovery_link_tag
+h5. Linking to Feeds with +auto_discovery_link_tag+
The +auto_discovery_link_tag+ helper builds HTML that most browsers and newsreaders can use to detect the presences of RSS or ATOM feeds. It takes the type of the link (+:rss+ or +:atom+), a hash of options that are passed through to url_for, and a hash of options for the tag:
@@ -529,7 +543,7 @@ There are three tag options available for +auto_discovery_link_tag+:
* +:type+ specifies an explicit MIME type. Rails will generate an appropriate MIME type automatically
* +:title+ specifies the title of the link
-h5. Linking to Javascript Files with javascript_include_tag
+h5. Linking to Javascript Files with +javascript_include_tag+
The +javascript_include_tag+ helper returns an HTML +script+ tag for each source provided. Rails looks in +public/javascripts+ for these files by default, but you can specify a full path relative to the document root, or a URL, if you prefer. For example, to include +public/javascripts/main.js+:
@@ -588,7 +602,7 @@ By default, the combined file will be delivered as +javascripts/all.js+. You can
You can even use dynamic paths such as +cache/#{current_site}/main/display+.
-h5. Linking to CSS Files with stylesheet_link_tag
+h5. Linking to CSS Files with +stylesheet_link_tag+
The +stylesheet_link_tag+ helper returns an HTML +<link>+ tag for each source provided. Rails looks in +public/stylesheets+ for these files by default, but you can specify a full path relative to the document root, or a URL, if you prefer. For example, to include +public/stylesheets/main.cs+:
@@ -647,7 +661,7 @@ By default, the combined file will be delivered as +stylesheets/all.css+. You ca
You can even use dynamic paths such as +cache/#{current_site}/main/display+.
-h5. Linking to Images with image_tag
+h5. Linking to Images with +image_tag+
The +image_tag+ helper builds an HTML +&lt;image&gt;+ tag to the specified file. By default, files are loaded from +public/images+. If you don't specify an extension, +.png+ is assumed by default:
@@ -673,7 +687,7 @@ There are also three special options you can use with +image_tag+:
* +:size+ specifies both width and height, in the format "{width}x{height}" (for example, "150x125")
* +:mouseover+ sets an alternate image to be used when the onmouseover event is fired.
-h4. Understanding yield
+h4. Understanding +yield+
Within the context of a layout, +yield+ identifies a section where content from the view should be inserted. The simplest way to use this is to have a single +yield+, into which the entire contents of the view currently being rendered is inserted:
@@ -702,7 +716,7 @@ You can also create a layout with multiple yielding regions:
The main body of the view will always render into the unnamed +yield+. To render content into a named +yield+, you use the +content_for+ method.
-h4. Using content_for
+h4. Using +content_for+
The +content_for+ method allows you to insert content into a +yield+ block in your layout. You only use +content_for+ to insert content in named yields. For example, this view would work with the layout that you just saw:
@@ -915,7 +929,7 @@ You may find that your application requires a layout that differs slightly from
Suppose you have the follow +ApplicationController+ layout:
-* +app/views/layouts/application.erb+
+* +app/views/layouts/application.html.erb+
<erb>
<html>
@@ -934,7 +948,7 @@ Suppose you have the follow +ApplicationController+ layout:
On pages generated by +NewsController+, you want to hide the top menu and add a right menu:
-* +app/views/layouts/news.erb+
+* +app/views/layouts/news.html.erb+
<erb>
<% content_for :stylesheets do %>
@@ -945,7 +959,7 @@ On pages generated by +NewsController+, you want to hide the top menu and add a
<div id="right_menu">Right menu items here</div>
<%= yield(:news_content) or yield %>
<% end -%>
-<% render :file => 'layouts/application' %>
+<%= render :file => 'layouts/application' %>
</erb>
That's it. The News views will use the new layout, hiding the top menu and adding a new right menu inside the "content" div.
diff --git a/railties/guides/source/migrations.textile b/railties/guides/source/migrations.textile
index 3f1ad51000..5ed94c30b7 100644
--- a/railties/guides/source/migrations.textile
+++ b/railties/guides/source/migrations.textile
@@ -15,7 +15,7 @@ You'll learn all about migrations including:
endprologue.
-h3. Anatomy Of A Migration
+h3. Anatomy of a Migration
Before I dive into the details of a migration, here are a few examples of the sorts of things you can do:
@@ -60,7 +60,7 @@ to have already opted in, so we use the User model to set the flag to +true+ for
NOTE: Some "caveats":#using-models-in-your-migrations apply to using models in your migrations.
-h4. Migrations are classes
+h4. Migrations are Classes
A migration is a subclass of <tt>ActiveRecord::Migration</tt> that implements two class methods: +up+ (perform the required transformations) and +down+ (revert them).
@@ -76,11 +76,11 @@ Active Record provides methods that perform common data definition tasks in a da
* +add_index+
* +remove_index+
-If you need to perform tasks specific to your database (for example create a "foreign key":#active-recordand-referential-integrity constraint) then the +execute+ function allows you to execute arbitrary SQL. A migration is just a regular Ruby class so you're not limited to these functions. For example after adding a column you could write code to set the value of that column for existing records (if necessary using your models).
+If you need to perform tasks specific to your database (for example create a "foreign key":#active-record-and-referential-integrity constraint) then the +execute+ function allows you to execute arbitrary SQL. A migration is just a regular Ruby class so you're not limited to these functions. For example after adding a column you could write code to set the value of that column for existing records (if necessary using your models).
On databases that support transactions with statements that change the schema (such as PostgreSQL), migrations are wrapped in a transaction. If the database does not support this (for example MySQL and SQLite) then when a migration fails the parts of it that succeeded will not be rolled back. You will have to unpick the changes that were made by hand.
-h4. What's in a name
+h4. What's in a Name
Migrations are stored in files in +db/migrate+, one for each migration class. The name of the file is of the form +YYYYMMDDHHMMSS_create_products.rb+, that is to say a UTC timestamp identifying the migration followed by an underscore followed by the name of the migration. The migration class' name must match (the camelcased version of) the latter part of the file name. For example +20080906120000_create_products.rb+ should define +CreateProducts+ and +20080906120001_add_details_to_products.rb+ should define +AddDetailsToProducts+. If you do feel the need to change the file name then you <em>have to</em> update the name of the class inside or Rails will complain about a missing class.
@@ -92,15 +92,15 @@ For example Alice adds migrations +20080906120000+ and +20080906123000+ and Bob
Of course this is no substitution for communication within the team. For example, if Alice's migration removed a table that Bob's migration assumed to exist, then trouble would certainly strike.
-h4. Changing migrations
+h4. Changing Migrations
Occasionally you will make a mistake when writing a migration. If you have already run the migration then you cannot just edit the migration and run the migration again: Rails thinks it has already run the migration and so will do nothing when you run +rake db:migrate+. You must rollback the migration (for example with +rake db:rollback+), edit your migration and then run +rake db:migrate+ to run the corrected version.
In general editing existing migrations is not a good idea: you will be creating extra work for yourself and your co-workers and cause major headaches if the existing version of the migration has already been run on production machines. Instead you should write a new migration that performs the changes you require. Editing a freshly generated migration that has not yet been committed to source control (or more generally which has not been propagated beyond your development machine) is relatively harmless. Just use some common sense.
-h3. Creating A Migration
+h3. Creating a Migration
-h4. Creating a model
+h4. Creating a Model
The model and scaffold generators will create migrations appropriate for adding a new model. This migration will already contain instructions for creating the relevant table. If you tell Rails what columns you want then statements for adding those will also be created. For example, running
@@ -130,7 +130,7 @@ end
You can append as many column name/type pairs as you want. By default +t.timestamps+ (which creates the +updated_at+ and +created_at+ columns that
are automatically populated by Active Record) will be added for you.
-h4. Creating a standalone migration
+h4. Creating a Standalone Migration
If you are creating migrations for other purposes (for example to add a column to an existing table) then you can use the migration generator:
@@ -218,7 +218,7 @@ h3. Writing a Migration
Once you have created your migration using one of the generators it's time to get to work!
-h4. Creating a table
+h4. Creating a Table
Migration method +create_table+ will be one of your workhorses. A typical use would be
@@ -268,7 +268,7 @@ end
This may however hinder portability to other databases.
-h4. Changing tables
+h4. Changing Tables
A close cousin of +create_table+ is +change_table+, used for changing existing tables. It is used in a similar fashion to +create_table+ but the object yielded to the block knows more tricks. For example
@@ -292,7 +292,7 @@ rename_column :products, :upccode, :upc_code
You don't have to keep repeating the table name and it groups all the statements related to modifying one particular table. The individual transformation names are also shorter, for example +remove_column+ becomes just +remove+ and +add_index+ becomes just +index+.
-h4. Special helpers
+h4. Special Helpers
Active Record provides some shortcuts for common functionality. It is for example very common to add both the +created_at+ and +updated_at+ columns and so there is a method that does exactly that:
@@ -327,13 +327,13 @@ end
</ruby>
will add an +attachment_id+ column and a string +attachment_type+ column with a default value of 'Photo'.
-NOTE: The +references+ helper does not actually create foreign key constraints for you. You will need to use +execute+ for that or a plugin that adds "foreign key support":#active-recordand-referential-integrity.
+NOTE: The +references+ helper does not actually create foreign key constraints for you. You will need to use +execute+ for that or a plugin that adds "foreign key support":#active-record-and-referential-integrity.
If the helpers provided by Active Record aren't enough you can use the +execute+ function to execute arbitrary SQL.
-For more details and examples of individual methods check the API documentation, in particular the documentation for "<tt>ActiveRecord::ConnectionAdapters::SchemaStatements</tt>":http://api.rubyonrails.com/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html (which provides the methods available in the +up+ and +down+ methods), "<tt>ActiveRecord::ConnectionAdapters::TableDefinition</tt>":http://api.rubyonrails.com/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html (which provides the methods available on the object yielded by +create_table+) and "<tt>ActiveRecord::ConnectionAdapters::Table</tt>":http://api.rubyonrails.com/classes/ActiveRecord/ConnectionAdapters/Table.html (which provides the methods available on the object yielded by +change_table+).
+For more details and examples of individual methods check the API documentation, in particular the documentation for "<tt>ActiveRecord::ConnectionAdapters::SchemaStatements</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html (which provides the methods available in the +up+ and +down+ methods), "<tt>ActiveRecord::ConnectionAdapters::TableDefinition</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html (which provides the methods available on the object yielded by +create_table+) and "<tt>ActiveRecord::ConnectionAdapters::Table</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html (which provides the methods available on the object yielded by +change_table+).
-h4. Writing your down method
+h4. Writing Your +down+ Method
The +down+ method of your migration should revert the transformations done by the +up+ method. In other words the database schema should be unchanged if you do an +up+ followed by a +down+. For example if you create a table in the +up+ method you should drop it in the +down+ method. It is wise to do things in precisely the reverse order to in the +up+ method. For example
@@ -384,7 +384,7 @@ rake db:migrate VERSION=20080906120000
If this is greater than the current version (i.e. it is migrating upwards) this will run the +up+ method on all migrations up to and including 20080906120000, if migrating downwards this will run the +down+ method on all the migrations down to, but not including, 20080906120000.
-h4. Rolling back
+h4. Rolling Back
A common task is to rollback the last migration, for example if you made a mistake in it and wish to correct it. Rather than tracking down the version number associated with the previous migration you can run
@@ -410,7 +410,7 @@ Neither of these Rake tasks do anything you could not do with +db:migrate+, they
Lastly, the +db:reset+ task will drop the database, recreate it and load the current schema into it.
-NOTE: This is not the same as running all the migrations - see the section on "schema.rb":#schemadumpingandyou.
+NOTE: This is not the same as running all the migrations - see the section on "schema.rb":#schema-dumping-and-you.
h4. Being Specific
@@ -422,7 +422,7 @@ rake db:migrate:up VERSION=20080906120000
will run the +up+ method from the 20080906120000 migration. These tasks check whether the migration has already run, so for example +db:migrate:up VERSION=20080906120000+ will do nothing if Active Record believes that 20080906120000 has already been run.
-h4. Being talkative
+h4. Being Talkative
By default migrations tell you exactly what they're doing and how long it took. A migration creating a table and adding an index might produce output like this
@@ -482,7 +482,7 @@ generates the following output
If you just want Active Record to shut up then running +rake db:migrate VERBOSE=false+ will suppress any output.
-h3. Using Models In Your Migrations
+h3. Using Models in Your Migrations
When creating or updating data in a migration it is often tempting to use one of your models. After all they exist to provide easy access to the underlying data. This can be done but some caution should be observed.
@@ -506,7 +506,7 @@ end
</ruby>
The migration has its own minimal copy of the +Product+ model and no longer cares about the +Product+ model defined in the application.
-h4. Dealing with changing models
+h4. Dealing with Changing Models
For performance reasons information about the columns a model has is cached. For example if you add a column to a table and then try and use the corresponding model to insert a new row it may try and use the old column information. You can force Active Record to re-read the column information with the +reset_column_information+ method, for example
@@ -528,9 +528,9 @@ end
</ruby>
-h3. Schema dumping and you
+h3. Schema Dumping and You
-h4. What are schema files for?
+h4. What are Schema Files for?
Migrations, mighty as they may be, are not the authoritative source for your database schema. That role falls to either +db/schema.rb+ or an SQL file which Active Record generates by examining the database. They are not designed to be edited, they just represent the current state of the database.
@@ -540,7 +540,7 @@ For example, this is how the test database is created: the current development d
Schema files are also useful if you want a quick look at what attributes an Active Record object has. This information is not in the model's code and is frequently spread across several migrations but is all summed up in the schema file. The "annotate_models":http://agilewebdevelopment.com/plugins/annotate_models plugin, which automatically adds (and updates) comments at the top of each model summarising the schema, may also be of interest.
-h4. Types of schema dumps
+h4. Types of Schema Dumps
There are two ways to dump the schema. This is set in +config/environment.rb+ by the +config.active_record.schema_format+ setting, which may be either +:sql+ or +:ruby+.
@@ -572,7 +572,7 @@ Instead of using Active Record's schema dumper the database's structure will be
By definition this will be a perfect copy of the database's structure but this will usually prevent loading the schema into a database other than the one used to create it.
-h4. Schema dumps and source control
+h4. Schema Dumps and Source Control
Because schema dumps are the authoritative source for your database schema, it is strongly recommended that you check them into source control.
diff --git a/railties/guides/source/nested_model_forms.textile b/railties/guides/source/nested_model_forms.textile
new file mode 100644
index 0000000000..4b685b214e
--- /dev/null
+++ b/railties/guides/source/nested_model_forms.textile
@@ -0,0 +1,222 @@
+h2. Rails nested model forms
+
+Creating a form for a model _and_ its associations can become quite tedious. Therefor Rails provides helpers to assist in dealing with the complexities of generating these forms _and_ the required CRUD operations to create, update, and destroy associations.
+
+In this guide you will:
+
+* do stuff
+
+endprologue.
+
+NOTE: This guide assumes the user knows how to use the "Rails form helpers":form_helpers.html in general. Also, it’s *not* an API reference. For a complete reference please visit "the Rails API documentation":http://api.rubyonrails.org/.
+
+
+h3. Model setup
+
+To be able to use the nested model functionality in your forms, the model will need to support some basic operations.
+
+First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The +fields_for+ form helper will look for this method to decide whether or not a nested model form should be build.
+
+If the associated object is an array a form builder will be yielded for each object, else only a single form builder will be yielded.
+
+Consider a Person model with an associated Address. When asked to yield a nested FormBuilder for the +:address+ attribute, the +fields_for+ form helper will look for a method on the Person instance named +address_attributes=+.
+
+h4. ActiveRecord::Base model
+
+For an ActiveRecord::Base model and association this writer method is commonly defined with the +accepts_nested_attributes_for+ class method:
+
+h5. has_one
+
+<ruby>
+class Person < ActiveRecord::Base
+ has_one :address
+ accepts_nested_attributes_for :address
+end
+</ruby>
+
+h5. belongs_to
+
+<ruby>
+class Person < ActiveRecord::Base
+ belongs_to :firm
+ accepts_nested_attributes_for :firm
+end
+</ruby>
+
+h5. has_many / has_and_belongs_to_many
+
+<ruby>
+class Person < ActiveRecord::Base
+ has_many :projects
+ accepts_nested_attributes_for :projects
+end
+</ruby>
+
+h4. Custom model
+
+As you might have inflected from this explanation, you _don’t_ necessarily need an ActiveRecord::Base model to use this functionality. The following examples are sufficient to enable the nested model form behaviour:
+
+h5. Single associated object
+
+<ruby>
+class Person
+ def address
+ Address.new
+ end
+
+ def address_attributes=(attributes)
+ # ...
+ end
+end
+</ruby>
+
+h5. Association collection
+
+<ruby>
+class Person
+ def projects
+ [Project.new, Project.new]
+ end
+
+ def projects_attributes=(attributes)
+ # ...
+ end
+end
+</ruby>
+
+NOTE: See (TODO) in the advanced section for more information on how to deal with the CRUD operations in your custom model.
+
+h3. Views
+
+h4. Controller code
+
+A nested model form will _only_ be build if the associated object(s) exist. This means that for a new model instance you would probably want to build the associated object(s) first.
+
+Consider the following typical RESTful controller which will prepare a new Person instance and its +address+ and +projects+ associations before rendering the +new+ template:
+
+<ruby>
+class PeopleController < ActionController:Base
+ def new
+ @person = Person.new
+ @person.built_address
+ 2.times { @person.projects.build }
+ end
+
+ def create
+ @person = Person.new(params[:person])
+ if @person.save
+ # ...
+ end
+ end
+end
+</ruby>
+
+NOTE: Obviously the instantiation of the associated object(s) can become tedious and not DRY, so you might want to move that into the model itself. ActiveRecord::Base provides an +after_initialize+ callback which is a good way to refactor this.
+
+h4. Form code
+
+Now that you have a model instance, with the appropriate methods and associated object(s), you can start building the nested model form.
+
+h5. Standard form
+
+Start out with a regular RESTful form:
+
+<erb>
+<% form_for @person do |f| %>
+ <%= f.text_field :name %>
+<% end %>
+</erb>
+
+This will generate the following html:
+
+<html>
+<form action="/people" class="new_person" id="new_person" method="post">
+ <input id="person_name" name="person[name]" size="30" type="text" />
+</form>
+</html>
+
+h5. Nested form for a single associated object
+
+Now add a nested form for the +address+ association:
+
+<erb>
+<% form_for @person do |f| %>
+ <%= f.text_field :name %>
+
+ <% f.fields_for :address do |af| %>
+ <%= f.text_field :street %>
+ <% end %>
+<% end %>
+</erb>
+
+This generates:
+
+<html>
+<form action="/people" class="new_person" id="new_person" method="post">
+ <input id="person_name" name="person[name]" size="30" type="text" />
+
+ <input id="person_address_attributes_street" name="person[address_attributes][street]" size="30" type="text" />
+</form>
+</html>
+
+Notice that +fields_for+ recognized the +address+ as an association for which a nested model form should be build by the way it has namespaced the +name+ attribute.
+
+When this form is posted the Rails parameter parser will construct a hash like the following:
+
+<ruby>
+{
+ "person" => {
+ "name" => "Eloy Duran",
+ "address_attributes" => {
+ "street" => "Nieuwe Prinsengracht"
+ }
+ }
+}
+</ruby>
+
+That’s it. The controller will simply pass this hash on to the model from the +create+ action. The model will then handle building the +address+ association for you and automatically save it when the parent (+person+) is saved.
+
+h5. Nested form for a collection of associated objects
+
+The form code for an association collection is pretty similar to that of a single associated object:
+
+<erb>
+<% form_for @person do |f| %>
+ <%= f.text_field :name %>
+
+ <% f.fields_for :projects do |pf| %>
+ <%= f.text_field :name %>
+ <% end %>
+<% end %>
+</erb>
+
+Which generates:
+
+<html>
+<form action="/people" class="new_person" id="new_person" method="post">
+ <input id="person_name" name="person[name]" size="30" type="text" />
+
+ <input id="person_projects_attributes_0_name" name="person[projects_attributes][0][name]" size="30" type="text" />
+ <input id="person_projects_attributes_1_name" name="person[projects_attributes][1][name]" size="30" type="text" />
+</form>
+</html>
+
+As you can see it has generated 2 +project name+ inputs, one for each new +project+ that’s build in the controllers +new+ action. Only this time the +name+ attribute of the input contains a digit as an extra namespace. This will be parsed by the Rails parameter parser as:
+
+<ruby>
+{
+ "person" => {
+ "name" => "Eloy Duran",
+ "projects_attributes" => {
+ "0" => { "name" => "Project 1" },
+ "1" => { "name" => "Project 2" }
+ }
+ }
+}
+</ruby>
+
+You can basically see the +projects_attributes+ hash as an array of attribute hashes. One for each model instance.
+
+NOTE: The reason that +fields_for+ constructed a form which would result in a hash instead of an array is that it won't work for any forms nested deeper than one level deep.
+
+TIP: You _can_ however pass an array to the writer method generated by +accepts_nested_attributes_for+ if you're using plain Ruby or some other API access. See (TODO) for more info and example. \ No newline at end of file
diff --git a/railties/guides/source/performance_testing.textile b/railties/guides/source/performance_testing.textile
index fa1ca8bde2..320a5b8472 100644
--- a/railties/guides/source/performance_testing.textile
+++ b/railties/guides/source/performance_testing.textile
@@ -32,7 +32,7 @@ end
This example is a simple performance test case for profiling a GET request to the application's homepage.
-h4. Generating performance tests
+h4. Generating Performance Tests
Rails provides a generator called +performance_test+ for creating new performance tests:
@@ -95,7 +95,7 @@ class Post < ActiveRecord::Base
end
</ruby>
-h5. Controller example
+h5. Controller Example
Because performance tests are a special kind of integration test, you can use the +get+ and +post+ methods in them.
@@ -123,7 +123,7 @@ end
You can find more details about the +get+ and +post+ methods in the "Testing Rails Applications":testing.html guide.
-h5. Model example
+h5. Model Example
Even though the performance tests are integration tests and hence closer to the request/response cycle by nature, you can still performance test pure model code.
@@ -173,13 +173,13 @@ h4. Metrics
Benchmarking and profiling run performance tests in various modes described below.
-h5. Wall time
+h5. Wall Time
Wall time measures the real world time elapsed during the test run. It is affected by any other processes concurrently running on the system.
Mode: Benchmarking
-h5. Process time
+h5. Process Time
Process time measures the time taken by the process. It is unaffected by any other processes running concurrently on the same system. Hence, process time is likely to be constant for any given performance test, irrespective of the machine load.
@@ -197,19 +197,19 @@ Objects measures the number of objects allocated for the performance test case.
Mode: Benchmarking, Profiling "Requires GC Patched Ruby":#installing-gc-patched-ruby
-h5. GC runs
+h5. GC Runs
GC Runs measures the number of times GC was invoked for the performance test case.
Mode: Benchmarking "Requires GC Patched Ruby":#installing-gc-patched-ruby
-h5. GC time
+h5. GC Time
GC Time measures the amount of time spent in GC for the performance test case.
Mode: Benchmarking "Requires GC Patched Ruby":#installing-gc-patched-ruby
-h4. Understanding the output
+h4. Understanding the Output
Performance tests generate different outputs inside +tmp/performance+ directory depending on their mode and metric.
@@ -217,7 +217,7 @@ h5. Benchmarking
In benchmarking mode, performance tests generate two types of outputs:
-h6. Command line
+h6. Command Line
This is the primary form of output in benchmarking mode. Example:
@@ -230,7 +230,7 @@ BrowsingTest#test_homepage (31 ms warmup)
gc_time: 19 ms
</shell>
-h6. CSV files
+h6. CSV Files
Performance test results are also appended to +.csv+ files inside +tmp/performance+. For example, running the default +BrowsingTest#test_homepage+ will generate following five files:
@@ -262,7 +262,7 @@ h5. Profiling
In profiling mode, you can choose from four types of output.
-h6. Command line
+h6. Command Line
This is a very basic form of output in profiling mode:
@@ -285,13 +285,13 @@ h6. Tree
Tree output is profiling information in calltree format for use by "kcachegrind":http://kcachegrind.sourceforge.net/html/Home.html and similar tools.
-h4. Tuning test runs
+h4. Tuning Test Runs
By default, each performance test is run +4 times+ in benchmarking mode and +1 time+ in profiling. However, test runs can easily be configured.
WARNING: Performance test configurability is not yet enabled in Rails. But it will be soon.
-h4. Performance test environment
+h4. Performance Test Environment
Performance tests are run in the +development+ environment. But running performance tests will set the following configuration parameters:
@@ -303,7 +303,7 @@ Rails.logger.level = ActiveSupport::BufferedLogger::INFO
As +ActionController::Base.perform_caching+ is set to +true+, performance tests will behave much as they do in the +production+ environment.
-h4. Installing GC-patched Ruby
+h4. Installing GC-Patched Ruby
To get the best from Rails performance tests, you need to build a special Ruby binary with some super powers - "GC patch":http://rubyforge.org/tracker/download.php/1814/7062/17676/3291/ruby186gc.patch for measuring GC Runs/Time and memory/object allocation.
@@ -313,7 +313,7 @@ h5. Installation
Compile Ruby and apply this "GC Patch":http://rubyforge.org/tracker/download.php/1814/7062/17676/3291/ruby186gc.patch.
-h5. Download and extract
+h5. Download and Extract
<shell>
[lifo@null ~]$ mkdir rubygc
@@ -322,13 +322,13 @@ h5. Download and extract
[lifo@null ~]$ cd <ruby-version>
</shell>
-h5. Apply the patch
+h5. Apply the Patch
<shell>
[lifo@null ruby-version]$ curl http://rubyforge.org/tracker/download.php/1814/7062/17676/3291/ruby186gc.patch | patch -p0
</shell>
-h5. Configure and install
+h5. Configure and Install
The following will install ruby in your home directory's +/rubygc+ directory. Make sure to replace +<homedir>+ with a full patch to your actual home directory.
@@ -337,7 +337,7 @@ The following will install ruby in your home directory's +/rubygc+ directory. Ma
[lifo@null ruby-version]$ make && make install
</shell>
-h5. Prepare aliases
+h5. Prepare Aliases
For convenience, add the following lines in your +~/.profile+:
@@ -349,7 +349,7 @@ alias gcirb='~/rubygc/bin/irb'
alias gcrails='~/rubygc/bin/rails'
</shell>
-h5. Install Rubygems and dependency gems
+h5. Install Rubygems and Dependency Gems
Download "Rubygems":http://rubyforge.org/projects/rubygems and install it from source. Rubygem's README file should have necessary installation instructions.
@@ -446,11 +446,11 @@ This benchmarks the code enclosed in the +Project.benchmark("Creating project")
Creating project (185.3ms)
</ruby>
-Please refer to the "API docs":http://api.rubyonrails.com/classes/ActiveRecord/Base.html#M001336 for additional options to +benchmark()+
+Please refer to the "API docs":http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M001336 for additional options to +benchmark()+
h4. Controller
-Similarly, you could use this helper method inside "controllers":http://api.rubyonrails.com/classes/ActionController/Benchmarking/ClassMethods.html#M000715
+Similarly, you could use this helper method inside "controllers":http://api.rubyonrails.org/classes/ActionController/Benchmarking/ClassMethods.html#M000715
<ruby>
def process_projects
@@ -465,7 +465,7 @@ NOTE: +benchmark+ is a class method inside controllers
h4. View
-And in "views":http://api.rubyonrails.com/classes/ActionController/Benchmarking/ClassMethods.html#M000715:
+And in "views":http://api.rubyonrails.org/classes/ActionController/Benchmarking/ClassMethods.html#M000715:
<erb>
<% benchmark("Showing projects partial") do %>
@@ -496,21 +496,21 @@ Michael Koziarski has an "interesting blog post":http://www.therailsway.com/2009
h3. Useful Links
-h4. Rails plugins and gems
+h4. Rails Plugins and Gems
* "Rails Analyzer":http://rails-analyzer.rubyforge.org
-* "Palmist":http://www.flyingmachinestudios.com/projects
+* "Palmist":http://www.flyingmachinestudios.com/projects/
* "Rails Footnotes":http://github.com/josevalim/rails-footnotes/tree/master
* "Query Reviewer":http://github.com/dsboulder/query_reviewer/tree/master
-h4. Generic tools
+h4. Generic Tools
-* "httperf":http://www.hpl.hp.com/research/linux/httperf
+* "httperf":http://www.hpl.hp.com/research/linux/httperf/
* "ab":http://httpd.apache.org/docs/2.2/programs/ab.html
-* "JMeter":http://jakarta.apache.org/jmeter
+* "JMeter":http://jakarta.apache.org/jmeter/
* "kcachegrind":http://kcachegrind.sourceforge.net/html/Home.html
-h4. Tutorials and documentation
+h4. Tutorials and Documentation
* "ruby-prof API Documentation":http://ruby-prof.rubyforge.org
* "Request Profiling Railscast":http://railscasts.com/episodes/98-request-profiling - Outdated, but useful for understanding call graphs
diff --git a/railties/guides/source/plugins.textile b/railties/guides/source/plugins.textile
index d2fb157e35..55ecdcd3d1 100644
--- a/railties/guides/source/plugins.textile
+++ b/railties/guides/source/plugins.textile
@@ -31,7 +31,7 @@ endprologue.
h3. Setup
-h4. Create the basic app
+h4. Create the Basic Application
The examples in this guide require that you have a working rails application. To create a simple rails app execute:
@@ -49,7 +49,7 @@ Then navigate to http://localhost:3000/birds. Make sure you have a functioning
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.
-h4. Generate the plugin skeleton
+h4. Generate the Plugin Skeleton
Rails ships with a plugin generator which creates a basic plugin skeleton. Pass the plugin name, either 'CamelCased' or 'under_scored', as an argument. Pass +--with-generator+ to add an example generator also.
@@ -91,7 +91,7 @@ create vendor/plugins/yaffle/generators/yaffle/yaffle_generator.rb
create vendor/plugins/yaffle/generators/yaffle/USAGE
</pre>
-h4. Organize your files
+h4. Organize Your Files
To make it easy to organize your files and to make the plugin more compatible with GemPlugins, start out by altering your file system to look like this:
@@ -211,7 +211,7 @@ end
Now whenever you write a test that requires the database, you can call 'load_schema'.
-h4. Run the plugin tests
+h4. Run the Plugin Tests
Once you have these files in place, you can write your first test to ensure that your plugin-testing setup is correct. By default rails generates a file in 'vendor/plugins/yaffle/test/yaffle_test.rb' with a sample test. Replace the contents of that file with:
@@ -275,7 +275,7 @@ rake DB=postgresql
Now you are ready to test-drive your plugin!
-h3. Extending core classes
+h3. Extending Core Classes
This section will explain how to add a method to String that will be available anywhere in your rails app.
@@ -339,7 +339,7 @@ $ ./script/console
=> "squawk! Hello World"
</shell>
-h4. Working with init.rb
+h4. Working with +init.rb+
When rails loads plugins it looks for the file named 'init.rb' or 'rails/init.rb'. However, when the plugin is initialized, 'init.rb' is invoked via +eval+ (not +require+) so it has slightly different behavior.
@@ -369,7 +369,7 @@ class ::Hash
end
</ruby>
-h3. Add an 'acts_as' method to Active Record
+h3. Add an "acts_as" Method to Active Record
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.
@@ -425,7 +425,7 @@ 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+).
-h4. Add a class method
+h4. Add a Class Method
This plugin will expect that you've added a method to your model named 'last_squawk'. However, the plugin users might have already defined a method on their model named 'last_squawk' that they use for something else. This plugin will allow the name to be changed by adding a class method called 'yaffle_text_field'.
@@ -478,7 +478,7 @@ end
ActiveRecord::Base.send :include, Yaffle
</ruby>
-h4. Add an instance method
+h4. Add an Instance Method
This plugin will add a method named 'squawk' to any Active Record objects that call 'acts_as_yaffle'. The 'squawk' method will simply set the value of one of the fields in the database.
@@ -800,7 +800,7 @@ Many plugins ship with generators. When you created the plugin above, you speci
Building generators is a complex topic unto itself and this section will cover one small aspect of generators: generating a simple text file.
-h4. Testing generators
+h4. Testing Generators
Many rails plugin authors do not test their generators, however testing generators is quite simple. A typical generator test does the following:
@@ -864,7 +864,7 @@ class YaffleDefinitionGenerator < Rails::Generator::Base
end
</ruby>
-h4. The USAGE file
+h4. The +USAGE+ File
If you plan to distribute your plugin, developers will expect at least a minimum of documentation. You can add simple documentation to the generator by updating the USAGE file.
@@ -891,7 +891,7 @@ Description:
Adds a file with the definition of a Yaffle to the app's main directory
</shell>
-h3. Add a custom generator command
+h3. Add a Custom Generator Command
You may have noticed above that you can used one of the built-in rails migration commands +migration_template+. If your plugin needs to add and remove lines of text from existing files you will need to write your own generator methods.
@@ -1144,7 +1144,7 @@ end
Here are a few possibilities for how to allow developers to use your plugin migrations:
-h4. Create a custom rake task
+h4. Create a Custom Rake Task
* *vendor/plugins/yaffle/tasks/yaffle_tasks.rake:*
@@ -1165,7 +1165,7 @@ namespace :db do
end
</ruby>
-h4. Call migrations directly
+h4. Call Migrations Directly
* *vendor/plugins/yaffle/lib/yaffle.rb:*
@@ -1191,7 +1191,7 @@ end
NOTE: several plugin frameworks such as Desert and Engines provide more advanced plugin functionality.
-h4. Generate migrations
+h4. Generate Migrations
Generating migrations has several advantages over other methods. Namely, you can allow other developers to more easily customize the migration. The flow looks like this:
@@ -1417,7 +1417,7 @@ h4. References
* http://www.mbleigh.com/2008/6/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins
* http://weblog.jamisbuck.org/2006/10/26/monkey-patching-rails-extending-routes-2.
-h4. Contents of 'lib/yaffle.rb'
+h4. Contents of +lib/yaffle.rb+
* *vendor/plugins/yaffle/lib/yaffle.rb:*
@@ -1440,7 +1440,7 @@ end
# end
</ruby>
-h4. Final plugin directory structure
+h4. Final Plugin Directory Structure
The final plugin should have a directory structure that looks something like this:
diff --git a/railties/guides/source/rails_on_rack.textile b/railties/guides/source/rails_on_rack.textile
index e300e047b4..05581f943f 100644
--- a/railties/guides/source/rails_on_rack.textile
+++ b/railties/guides/source/rails_on_rack.textile
@@ -9,13 +9,13 @@ This guide covers Rails integration with Rack and interfacing with other Rack co
endprologue.
-WARNING: This guide assumes a working knowledge of Rack protocol and Rack concepts such as middlewares, url maps and Rack::Builder.
+WARNING: This guide assumes a working knowledge of Rack protocol and Rack concepts such as middlewares, url maps and +Rack::Builder+.
h3. Introduction to Rack
bq. Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.
-- "Rack API Documentation":http://rack.rubyforge.org/doc
+- "Rack API Documentation":http://rack.rubyforge.org/doc/
Explaining Rack is not really in the scope of this guide. In case you are not familiar with Rack's basics, you should check out the following links:
@@ -30,7 +30,7 @@ h4. Rails Application's Rack Object
<tt>ActionController::Dispatcher.new</tt> is the primary Rack application object of a Rails application. Any Rack compliant web server should be using +ActionController::Dispatcher.new+ object to serve a Rails application.</p>
-h4. script/server
+h4. +script/server+
<tt>script/server</tt> does the basic job of creating a +Rack::Builder+ object and starting the webserver. This is Rails' equivalent of Rack's +rackup+ script.
@@ -51,11 +51,11 @@ app = Rack::Builder.new {
Middlewares used in the code above are primarily useful only in the development envrionment. The following table explains their usage:
|_.Middleware|_.Purpose|
-|Rails::Rack::LogTailer|Appends log file output to console|
-|Rails::Rack::Static|Serves static files inside +RAILS_ROOT/public+ directory|
-|Rails::Rack::Debugger|Starts Debugger|
+|+Rails::Rack::LogTailer+|Appends log file output to console|
+|+Rails::Rack::Static+|Serves static files inside +RAILS_ROOT/public+ directory|
+|+Rails::Rack::Debugger+|Starts Debugger|
-h4. rackup
+h4. +rackup+
To use +rackup+ instead of Rails' +script/server+, you can put the following inside +config.ru+ of your Rails application's root directory:
@@ -109,7 +109,7 @@ use ActiveRecord::QueryCache
run ActionController::Dispatcher.new
</ruby>
-Purpose of each of this middlewares is explained in "Internal Middlewares":#internal-middleware-stack section.
+Purpose of each of this middlewares is explained in the "Internal Middlewares":#internal-middleware-stack section.
h4. Configuring Middleware Stack
@@ -119,9 +119,7 @@ h5. Adding a Middleware
You can add a new middleware to the middleware stack using any of the following methods:
-* +config.middleware.add(new_middleware, args)+ - Adds the new middleware at the bottom of the middleware stack.
-
-* +config.middleware.insert(index, new_middleware, args)+ - Adds the new middleware at the position specified by +index+ in the middleware stack.
+* +config.middleware.use(new_middleware, args)+ - Adds the new middleware at the bottom of the middleware stack.
* +config.middleware.insert_before(existing_middleware, new_middleware, args)+ - Adds the new middleware before the specified existing middleware in the middleware stack.
@@ -130,7 +128,7 @@ You can add a new middleware to the middleware stack using any of the following
<strong>Example:</strong>
<ruby>
-# environment.rb
+# config/environment.rb
# Push Rack::BounceFavicon at the bottom
config.middleware.use Rack::BounceFavicon
@@ -147,25 +145,35 @@ You can swap an existing middleware in the middleware stack using +config.middle
<strong>Example:</strong>
<ruby>
-# environment.rb
+# config/environment.rb
# Replace ActionController::Failsafe with Lifo::Failsafe
config.middleware.swap ActionController::Failsafe, Lifo::Failsafe
</ruby>
+h5. Middleware Stack is an Array
+
+The middleware stack behaves just like a normal +Array+. You can use any +Array+ methods to insert, reorder, or remove items from the stack. Methods described in the section above are just convenience methods.
+
+For example, the following removes the middleware matching the supplied class name:
+
+<ruby>
+config.middleware.delete(middleware)
+</ruby>
+
h4. Internal Middleware Stack
Much of Action Controller's functionality is implemented as Middlewares. The following table explains the purpose of each of them:
|_.Middleware|_.Purpose|
-|Rack::Lock|Sets +env["rack.multithread"]+ flag to +true+ and wraps the application within a Mutex.|
-|ActionController::Failsafe|Returns HTTP Status +500+ to the client if an exception gets raised while dispatching.|
-|ActiveRecord::QueryCache|Enable the Active Record query cache.|
-|ActionController::Session::CookieStore|Uses the cookie based session store.|
-|ActionController::Session::MemCacheStore|Uses the memcached based session store.|
-|ActiveRecord::SessionStore|Uses the database based session store.|
-|Rack::MethodOverride|Sets HTTP method based on +_method+ parameter or +env["HTTP_X_HTTP_METHOD_OVERRIDE"]+.|
-|Rack::Head|Discards the response body if the client sends a +HEAD+ request.|
+|+Rack::Lock+|Sets +env["rack.multithread"]+ flag to +true+ and wraps the application within a Mutex.|
+|+ActionController::Failsafe+|Returns HTTP Status +500+ to the client if an exception gets raised while dispatching.|
+|+ActiveRecord::QueryCache+|Enable the Active Record query cache.|
+|+ActionController::Session::CookieStore+|Uses the cookie based session store.|
+|+ActionController::Session::MemCacheStore+|Uses the memcached based session store.|
+|+ActiveRecord::SessionStore+|Uses the database based session store.|
+|+Rack::MethodOverride+|Sets HTTP method based on +_method+ parameter or +env["HTTP_X_HTTP_METHOD_OVERRIDE"]+.|
+|+Rack::Head+|Discards the response body if the client sends a +HEAD+ request.|
TIP: It's possible to use any of the above middlewares in your custom Rack stack.
@@ -197,10 +205,32 @@ use Rack::Head
run ActionController::Dispatcher.new
</shell>
+h4. Using Rack Builder
+
+The following shows how to replace use +Rack::Builder+ instead of the Rails supplied +MiddlewareStack+.
+
+<strong>Clear the existing Rails middleware stack</strong>
+
+<ruby>
+# environment.rb
+config.middleware.clear
+</ruby>
+
+<br />
+<strong>Add a +config.ru+ file to +RAILS_ROOT+</strong>
+
+<ruby>
+# config.ru
+use MyOwnStackFromStratch
+run ActionController::Dispatcher.new
+</ruby>
+
h3. Rails Metal Applications
Rails Metal applications are minimal Rack applications specially designed for integrating with a typical Rails application. As Rails Metal Applications skip all of the Action Controller stack, serving a request has no overhead from the Rails framework itself. This is especially useful for infrequent cases where the performance of the full stack Rails framework is an issue.
+Ryan Bates' "railscast on Rails Metal":http://railscasts.com/episodes/150-rails-metal provides a nice walkthrough generating and using Rails Metal.
+
h4. Generating a Metal Application
Rails provides a generator called +metal+ for creating a new Metal application:
@@ -226,6 +256,8 @@ class Poller
end
</ruby>
+Metal applications within +app/metal+ folders in plugins will also be discovered and added to the list.
+
Metal applications are an optimization. You should make sure to "understand the related performance implications":http://weblog.rubyonrails.org/2008/12/20/performance-of-rails-metal before using it.
h4. Execution Order
@@ -244,10 +276,31 @@ def call(env)
end
</ruby>
-In the code above, +@metals+ is an ordered ( alphabetical ) hash of metal applications. Due to the alphabetical ordering, +aaa.rb+ will come before +bbb.rb+ in the metal chain.
+In the code above, +@metals+ is an ordered hash of metal applications. Due to the default alphabetical ordering, +aaa.rb+ will come before +bbb.rb+ in the metal chain.
+
+It is, however, possible to override the default ordering in your environment. Simply add a line like the following to +config/environment.rb+
+
+<ruby>
+config.metals = ["Bbb", "Aaa"]
+</ruby>
+
+Each string in the array should be the name of your metal class. If you do this then be warned that any metal applications not listed will not be loaded.
WARNING: Metal applications cannot return the HTTP Status +404+ to a client, as it is used for continuing the Metal chain execution. Please use normal Rails controllers or a custom middleware if returning +404+ is a requirement.
+h3. Resources
+
+h4. Learning Rack
+
+* "Official Rack Website":http://rack.github.com
+* "Introducing Rack":http://chneukirchen.org/blog/archive/2007/02/introducing-rack.html
+* "Ruby on Rack #1 - Hello Rack!":http://m.onkey.org/2008/11/17/ruby-on-rack-1
+* "Ruby on Rack #2 - The Builder":http://m.onkey.org/2008/11/18/ruby-on-rack-2-rack-builder
+
+h4. Understanding Middlewares
+
+* "Railscast on Rack Middlewares":http://railscasts.com/episodes/151-rack-middleware
+
h3. Changelog
"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/58
diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile
index c26a5cd6ee..e9adb4b308 100644
--- a/railties/guides/source/routing.textile
+++ b/railties/guides/source/routing.textile
@@ -39,7 +39,7 @@ Then the routing engine is the piece that translates that to a link to a URL suc
NOTE: Patient needs to be declared as a resource for this style of translation via a named route to be available.
-h3. Quick Tour of Routes.rb
+h3. Quick Tour of +routes.rb+
There are two components to routing in Rails: the routing engine itself, which is supplied as part of Rails, and the file +config/routes.rb+, which contains the actual routes that will be used by your application. Learning exactly what you can put in +routes.rb+ is the main topic of this guide, but before we dig in let's get a quick overview.
@@ -221,7 +221,7 @@ Although the conventions of RESTful routing are likely to be sufficient for many
You can also add additional routes via the +:member+ and +:collection+ options, which are discussed later in this guide.
-h5. Using :controller
+h5. Using +:controller+
The +:controller+ option lets you use a controller name that is different from the public-facing resource name. For example, this routing entry:
@@ -270,7 +270,7 @@ end
That would give you routing for +admin/photos+ and +admin/videos+ controllers.
-h5. Using :singular
+h5. Using +:singular+
If for some reason Rails isn't doing what you want in converting the plural resource name to a singular name in member routes, you can override its judgment with the +:singular+ option:
@@ -280,9 +280,9 @@ map.resources :teeth, :singular => "tooth"
TIP: Depending on the other code in your application, you may prefer to add additional rules to the +Inflector+ class instead.
-h5. Using :requirements
+h5. Using +:requirements+
-You an use the +:requirements+ option in a RESTful route to impose a format on the implied +:id+ parameter in the singular routes. For example:
+You can use the +:requirements+ option in a RESTful route to impose a format on the implied +:id+ parameter in the singular routes. For example:
<ruby>
map.resources :photos, :requirements => {:id => /[A-Z][A-Z][0-9]+/}
@@ -290,11 +290,11 @@ map.resources :photos, :requirements => {:id => /[A-Z][A-Z][0-9]+/}
This declaration constrains the +:id+ parameter to match the supplied regular expression. So, in this case, +/photos/1+ would no longer be recognized by this route, but +/photos/RR27+ would.
-h5. Using :conditions
+h5. Using +:conditions+
Conditions in Rails routing are currently used only to set the HTTP verb for individual routes. Although in theory you can set this for RESTful routes, in practice there is no good reason to do so. (You'll learn more about conditions in the discussion of classic routing later in this guide.)
-h5. Using :as
+h5. Using +:as+
The +:as+ option lets you override the normal naming for the actual generated paths. For example:
@@ -315,7 +315,7 @@ will recognize incoming URLs containing +image+ but route the requests to the Ph
NOTE: The helpers will be generated with the name of the resource, not the path name. So in this case, you'd still get +photos_path+, +new_photo_path+, and so on.
-h5. Using :path_names
+h5. Using +:path_names+
The +:path_names+ option lets you override the automatically-generated "new" and "edit" segments in URLs:
@@ -338,7 +338,7 @@ TIP: If you find yourself wanting to change this option uniformly for all of you
config.action_controller.resources_path_names = { :new => 'make', :edit => 'change' }
</ruby>
-h5. Using :path_prefix
+h5. Using +:path_prefix+
The +:path_prefix+ option lets you add additional parameters that will be prefixed to the recognized paths. For example, suppose each photo in your application belongs to a particular photographer. In that case, you might declare this route:
@@ -357,7 +357,7 @@ NOTE: In most cases, it's simpler to recognize URLs of this sort by creating nes
NOTE: You can also use +:path_prefix+ with non-RESTful routes.
-h5. Using :name_prefix
+h5. Using +:name_prefix+
You can use the :name_prefix option to avoid collisions between routes. This is most useful when you have two resources with the same name that use +:path_prefix+ to map differently. For example:
@@ -372,7 +372,7 @@ This combination will give you route helpers such as +photographer_photos_path+
NOTE: You can also use +:name_prefix+ with non-RESTful routes.
-h5. Using :only and :except
+h5. Using +:only+ and +:except+
By default, Rails creates routes for all seven of the default actions (index, show, new, create, edit, update, and destroy) for every RESTful route in your application. You can use the +:only+ and +:except+ options to fine-tune this behavior. The +:only+ option specifies that only certain routes should be generated:
@@ -432,7 +432,7 @@ In addition to the routes for magazines, this declaration will also create route
This will also create routing helpers such as +magazine_ads_url+ and +edit_magazine_ad_path+.
-h5. Using :name_prefix
+h5. Using +:name_prefix+
The +:name_prefix+ option overrides the automatically-generated prefix in nested route helpers. For example,
@@ -457,7 +457,7 @@ ads_url(@magazine)
edit_ad_path(@magazine, @ad)
</ruby>
-h5. Using :has_one and :has_many
+h5. Using +:has_one+ and +:has_many+
The +:has_one+ and +:has_many+ options provide a succinct notation for simple nested routes. Use +:has_one+ to nest a singleton resource, or +:has_many+ to nest a plural resource:
@@ -582,7 +582,7 @@ To add a member route, use the +:member+ option:
map.resources :photos, :member => { :preview => :get }
</ruby>
-This will enable Rails to recognize URLs such as +/photos/1/preview+ using the GET HTTP verb, and route them to the preview action of the Photos controller. It will also create a +preview_photo+ route helper.
+This will enable Rails to recognize URLs such as +/photos/1/preview+ using the GET HTTP verb, and route them to the preview action of the Photos controller. It will also create the +preview_photo_url+ and +preview_photo_path+ route helpers.
Within the hash of member routes, each route name specifies the HTTP verb that it will recognize. You can use +:get+, +:put+, +:post+, +:delete+, or +:any+ here. You can also specify an array of methods, if you need more than one but you don't want to allow just anything:
@@ -598,7 +598,7 @@ To add a collection route, use the +:collection+ option:
map.resources :photos, :collection => { :search => :get }
</ruby>
-This will enable Rails to recognize URLs such as +/photos/search+ using the GET HTTP verb, and route them to the search action of the Photos controller. It will also create a +search_photos+ route helper.
+This will enable Rails to recognize URLs such as +/photos/search+ using the GET HTTP verb, and route them to the search action of the Photos controller. It will also create the +search_photos_url+ and +search_photos_path+ route helpers.
Just as with member routes, you can specify an array of methods for a collection route:
@@ -614,7 +614,7 @@ To add a new route (one that creates a new resource), use the +:new+ option:
map.resources :photos, :new => { :upload => :post }
</ruby>
-This will enable Rails to recognize URLs such as +/photos/upload+ using the POST HTTP verb, and route them to the upload action of the Photos controller. It will also create a +upload_photos+ route helper.
+This will enable Rails to recognize URLs such as +/photos/upload+ using the POST HTTP verb, and route them to the upload action of the Photos controller. It will also create the +upload_photos_path+ and +upload_photos_url+ route helpers.
TIP: If you want to redefine the verbs accepted by one of the standard actions, you can do so by explicitly mapping that action. For example:<br/>+map.resources :photos, :new => { :new => :any }+<br/>This will allow the new action to be invoked by any request to +photos/new+, no matter what HTTP verb you use.
@@ -748,7 +748,7 @@ end
The importance of +map.with_options+ has declined with the introduction of RESTful routes.
-h3. Formats and respond_to
+h3. Formats and +respond_to+
There's one more way in which routing can do different things depending on differences in the incoming HTTP request: by issuing a response that corresponds to what the request specifies that it will accept. In Rails routing, you can control this with the special +:format+ parameter in the route.
@@ -796,7 +796,7 @@ h3. The Empty Route
Don't confuse the default routes with the empty route. The empty route has one specific purpose: to route requests that come in to the root of the web site. For example, if your site is example.com, then requests to +http://example.com+ or +http://example.com/+ will be handled by the empty route.
-h4. Using map.root
+h4. Using +map.root+
The preferred way to set up the empty route is with the +map.root+ command:
@@ -829,7 +829,7 @@ h3. Inspecting and Testing Routes
Routing in your application should not be a "black box" that you never open. Rails offers built-in tools for both inspecting and testing routes.
-h4. Seeing Existing Routes with rake
+h4. Seeing Existing Routes with +rake+
If you want a complete list of all of the available routes in your application, run the +rake routes+ command. This will dump all of your routes to the console, in the same order that they appear in +routes.rb+. For each route, you'll see:
@@ -851,7 +851,7 @@ TIP: You'll find that the output from +rake routes+ is much more readable if you
h4. Testing Routes
-Routes should be included in your testing strategy (just like the rest of your application). Rails offers three "built-in assertions":http://api.rubyonrails.com/classes/ActionController/Assertions/RoutingAssertions.html designed to make testing routes simpler:
+Routes should be included in your testing strategy (just like the rest of your application). Rails offers three "built-in assertions":http://api.rubyonrails.org/classes/ActionController/Assertions/RoutingAssertions.html designed to make testing routes simpler:
* +assert_generates+
* +assert_recognizes+
diff --git a/railties/guides/source/security.textile b/railties/guides/source/security.textile
index 98a42f1223..1b64cc1be7 100644
--- a/railties/guides/source/security.textile
+++ b/railties/guides/source/security.textile
@@ -23,13 +23,13 @@ The Gartner Group however estimates that 75% of attacks are at the web applicati
The threats against web applications include user account hijacking, bypass of access control, reading or modifying sensitive data, or presenting fraudulent content. Or an attacker might be able to install a Trojan horse program or unsolicited e-mail sending software, aim at financial enrichment or cause brand name damage by modifying company resources. In order to prevent attacks, minimize their impact and remove points of attack, first of all, you have to fully understand the attack methods in order to find the correct countermeasures. That is what this guide aims at.
-In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs and make updating and security checks a habit (check the <a href="#additionalresources">Additional Resources</a> chapter). I do it manually because that‘s how you find the nasty logical security problems.
+In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs and make updating and security checks a habit (check the <a href="#additional-resources">Additional Resources</a> chapter). I do it manually because that‘s how you find the nasty logical security problems.
h3. Sessions
A good place to start looking at security is with sessions, which can be vulnerable to particular attacks.
-h4. What are sessions?
+h4. What are Sessions?
-- _HTTP is a stateless protocol. Sessions make it stateful._
@@ -49,7 +49,7 @@ h4. Session id
A session id consists of the hash value of a random string. The random string is the current time, a random number between 0 and 1, the process id number of the Ruby interpreter (also basically a random number) and a constant string. Currently it is not feasible to brute-force Rails' session ids. To date MD5 is uncompromised, but there have been collisions, so it is theoretically possible to create another input text with the same hash value. But this has had no security impact to date.
-h4. Session hijacking
+h4. Session Hijacking
-- _Stealing a user's session id lets an attacker use the web application in the victim's name._
@@ -67,7 +67,7 @@ Hence, the cookie serves as temporary authentication for the web application. Ev
The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from $10–$1000 (depending on the available amount of funds), $0.40–$20 for credit card numbers, $1–$8 for online auction site accounts and $4–$30 for email passwords, according to the "Symantec Global Internet Security Threat Report":http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf.
-h4. Session guidelines
+h4. Session Guidelines
-- _Here are some general guidelines on sessions._
@@ -77,7 +77,7 @@ This will also be a good idea, if you modify the structure of an object and old
* _(highlight)Critical data should not be stored in session_. If the user clears his cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data.
-h4. Session storage
+h4. Session Storage
-- _Rails provides several storage mechanisms for the session hashes. The most important are ActiveRecordStore and CookieStore._
@@ -100,7 +100,7 @@ config.action_controller.session = {
There are, however, derivatives of CookieStore which encrypt the session hash, so the client cannot see it.
-h4. Replay attacks for CookieStore sessions
+h4. Replay Attacks for CookieStore Sessions
-- _Another sort of attack you have to be aware of when using CookieStore is the replay attack._
@@ -116,7 +116,7 @@ Including a nonce (a random value) in the session solves replay attacks. A nonce
The best _(highlight)solution against it is not to store this kind of data in a session, but in the database_. In this case store the credit in the database and the logged_in_user_id in the session.
-h4. Session fixation
+h4. Session Fixation
-- _Apart from stealing a user's session id, the attacker may fix a session id known to him. This is called session fixation._
@@ -131,7 +131,7 @@ This attack focuses on fixing a user's session id known to the attacker, and for
# As the new trap session is unused, the web application will require the user to authenticate.
# From now on, the victim and the attacker will co-use the web application with the same session: The session became valid and the victim didn't notice the attack.
-h4. Session fixation – Countermeasures
+h4. Session Fixation – Countermeasures
-- _One line of code will protect you from session fixation._
@@ -145,7 +145,7 @@ If you use the popular RestfulAuthentication plugin for user management, add res
Another countermeasure is to _(highlight)save user-specific properties in the session_, verify them every time a request comes in, and deny access, if the information does not match. Such properties could be the remote IP address or the user agent (the web browser name), though the latter is less user-specific. When saving the IP address, you have to bear in mind that there are Internet service providers or large organizations that put their users behind proxies. _(highlight)These might change over the course of a session_, so these users will not be able to use your application, or only in a limited way.
-h4. Session expiry
+h4. Session Expiry
-- _Sessions that never expire extend the time-frame for attacks such as cross-site reference forgery (CSRF), session hijacking and session fixation._
@@ -172,7 +172,7 @@ self.delete_all "updated_at < '#{time.to_s(:db)}' OR
created_at < '#{2.days.ago.to_s(:db)}'"
</ruby>
-h3. Cross-Site Reference Forgery (CSRF)
+h3. Cross-Site Request Forgery (CSRF)
-- _This attack method works by including malicious code or a link in a page that accesses a web application that the user is believed to have authenticated. If the session for that web application has not timed out, an attacker may execute unauthorized commands._
@@ -278,7 +278,7 @@ Another redirection and self-contained XSS attack works in Firefox and Opera by
This example is a Base64 encoded JavaScript which displays a simple message box. In a redirection URL, an attacker could redirect to this URL with the malicious code in it. As a countermeasure, _(highlight)do not allow the user to supply (parts of) the URL to be redirected to_.
-h4. File uploads
+h4. File Uploads
-- _Make sure file uploads don't overwrite important files, and process media files asynchronously._
@@ -303,7 +303,7 @@ A significant disadvantage of synchronous processing of file uploads (as the att
The solution to this is best to _(highlight)process media files asynchronously_: Save the media file and schedule a processing request in the database. A second process will handle the processing of the file in the background.
-h4. Executable code in file uploads
+h4. Executable Code in File Uploads
-- _Source code in uploaded files may be executed when placed in specific directories. Do not place file uploads in Rails' /public directory if it is Apache's home directory._
@@ -311,7 +311,7 @@ The popular Apache web server has an option called DocumentRoot. This is the hom
_(highlight)If your Apache DocumentRoot points to Rails' /public directory, do not put file uploads in it_, store files at least one level downwards.
-h4. File downloads
+h4. File Downloads
-- _Make sure users cannot download arbitrary files._
@@ -333,11 +333,11 @@ send_file filename, :disposition => 'inline'
Another (additional) approach is to store the file names in the database and name the files on the disk after the ids in the database. This is also a good approach to avoid possible code in an uploaded file to be executed. The attachment_fu plugin does this in a similar way.
-h3. Intranet and Admin security
+h3. Intranet and Admin Security
-- _Intranet and administration interfaces are popular attack targets, because they allow privileged access. Although this would require several extra-security measures, the opposite is the case in the real world._
-In 2007 there was the first tailor-made "Trojan":http://www.symantec.com/enterprise/security_response/weblog/2007/08/a_monster_trojan.html which stole information from an Intranet, namely the "Monster for employers" web site of Monster.com, an online recruitment web application. Tailor-made Trojans are very rare, so far, and the risk is quite low, but it is certainly a possibility and an example of how the security of the client host is important, too. However, the highest threat to Intranet and Admin applications are XSS and CSRF.

+In 2007 there was the first tailor-made trojan which stole information from an Intranet, namely the "Monster for employers" web site of Monster.com, an online recruitment web application. Tailor-made Trojans are very rare, so far, and the risk is quite low, but it is certainly a possibility and an example of how the security of the client host is important, too. However, the highest threat to Intranet and Admin applications are XSS and CSRF.

*XSS* If your application re-displays malicious user input from the extranet, the application will be vulnerable to XSS. User names, comments, spam reports, order addresses are just a few uncommon examples, where there can be XSS.
@@ -347,15 +347,15 @@ Refer to the Injection section for countermeasures against XSS. It is _(highligh
*CSRF* Cross-Site Reference Forgery (CSRF) is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface.
-A real-world example is a "router reconfiguration by CSRF":http://www.symantec.com/enterprise/security_response/weblog/2008/01/driveby_pharming_in_the_
wild.html. The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for them, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had his credentials stolen.
+A real-world example is a "router reconfiguration by CSRF":http://www.h-online.com/security/Symantec-reports-first-active-attack-on-a-DSL-router--/news/102352. The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for them, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had his credentials stolen.
-Another example changed Google Adsense's e-mail address and password by "CSRF":http://www.0x000000.com/index.php?i=213&bin=11010101. If the victim was logged into Google Adsense, the administration interface for Google advertisements campaigns, an attacker could change his credentials.

+Another example changed Google Adsense's e-mail address and password by. If the victim was logged into Google Adsense, the administration interface for Google advertisements campaigns, an attacker could change his credentials.

Another popular attack is to spam your web application, your blog or forum to propagate malicious XSS. Of course, the attacker has to know the URL structure, but most Rails URLs are quite straightforward or they will be easy to find out, if it is an open-source application's admin interface. The attacker may even do 1,000 lucky guesses by just including malicious IMG-tags which try every possible combination.
For _(highlight)countermeasures against CSRF in administration interfaces and Intranet applications, refer to the countermeasures in the CSRF section_.
-h4. Additional precautions
+h4. Additional Precautions
The common admin interface works like this: it's located at www.example.com/admin, may be accessed only if the admin flag is set in the User model, re-displays user input and allows the admin to delete/add/edit whatever data desired. Here are some thoughts about this:
@@ -365,7 +365,7 @@ The common admin interface works like this: it's located at www.example.com/admi
* _(highlight)Put the admin interface to a special sub-domain_ such as admin.application.com and make it a separate application with its own user management. This makes stealing an admin cookie from the usual domain, www.application.com, impossible. This is because of the same origin policy in your browser: An injected (XSS) script on www.application.com may not read the cookie for admin.application.com and vice-versa.
-h3. Mass assignment
+h3. Mass Assignment
-- _Without any precautions Model.new(params[:model]) allows attackers to set any database column's value._
@@ -440,7 +440,7 @@ ActiveRecord::Base.send(:attr_accessible, nil)
This will create an empty whitelist of attributes available for mass assignment for all models in your app. As such, your models will need to explicitly whitelist accessible parameters by using an +attr_accessible+ declaration. This technique is best applied at the start of a new project. However, for an existing project with a thorough set of functional tests, it should be straightforward and relatively quick to insert this initializer, run your tests, and expose each attribute (via +attr_accessible+) as dictated by your failing tests.
-h3. User management
+h3. User Management
-- _Almost every web application has to deal with authorization and authentication. Instead of rolling your own, it is advisable to use common plug-ins. But keep them up-to-date, too. A few additional precautions can make your application even more secure._
@@ -467,7 +467,7 @@ SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1
And thus it found the first user in the database, returned it and logged him in. You can find out more about it in "my blog post":http://www.rorsecurity.info/2007/10/28/restful_authentication-login-security/. _(highlight)It is advisable to update your plug-ins from time to time_. Moreover, you can review your application to find more flaws like this.
-h4. Brute-forcing accounts
+h4. Brute-Forcing Accounts
-- _Brute-force attacks on accounts are trial and error attacks on the login credentials. Fend them off with more generic error messages and possibly require to enter a CAPTCHA._
@@ -479,7 +479,7 @@ However, what most web application designers neglect, are the forgot-password pa
In order to mitigate such attacks, _(highlight)display a generic error message on forgot-password pages, too_. Moreover, you can _(highlight)require to enter a CAPTCHA after a number of failed logins from a certain IP address_. Note, however, that this is not a bullet-proof solution against automatic programs, because these programs may change their IP address exactly as often. However, it raises the barrier of an attack.
-h4. Account hijacking
+h4. Account Hijacking
-- _Many web applications make it easy to hijack user accounts. Why not be different and make it more difficult?_
@@ -532,7 +532,7 @@ By default, Rails logs all requests being made to the web application. But log f
filter_parameter_logging :password
</ruby>
-h4. Good passwords
+h4. Good Passwords
-- _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._
@@ -544,7 +544,7 @@ It is interesting that only 4% of these passwords were dictionary words and the
A good password is a long alphanumeric combination of mixed cases. As this is quite hard to remember, it is advisable to enter only the _(highlight)first letters of a sentence that you can easily remember_. For example "The quick brown fox jumps over the lazy dog" will be "Tqbfjotld". Note that this is just an example, you should not use well known phrases like these, as they might appear in cracker dictionaries, too.
-h4. Regular expressions
+h4. Regular Expressions
-- _A common pitfall in Ruby's regular expressions is to match the string's beginning and end by ^ and $, instead of \A and \z._
@@ -568,7 +568,7 @@ Whereas %0A is a line feed in URL encoding, so Rails automatically converts it t
/\A[\w\.\-\+]+\z/
</ruby>
-h4. Privilege escalation
+h4. Privilege Escalation
-- _Changing a single parameter may give the user unauthorized access. Remember that every parameter may be changed, no matter how much you hide or obfuscate it._
@@ -629,7 +629,7 @@ SELECT * FROM projects WHERE name = '' OR 1 --'
The two dashes start a comment ignoring everything after it. So the query returns all records from the projects table including those blind to the user. This is because the condition is true for all records.
-h5. Bypassing authorization
+h5. Bypassing Authorization
Usually a web application includes access control. The user enters his login credentials, the web applications tries to find the matching record in the users table. The application grants access when it finds a record. However, an attacker may possibly bypass this check with SQL injection. The following shows a typical database query in Rails to find the first record in the users table which matches the login credentials parameters supplied by the user.
@@ -645,7 +645,7 @@ SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'&gt;'1'
This will simply find the first record in the database, and grants access to this user.
-h5. Unauthorized reading
+h5. Unauthorized Reading
The UNION statement connects two SQL queries and returns the data in one set. An attacker can use it to read arbitrary data from the database. Let's take the example from above:
@@ -692,7 +692,7 @@ h4. Cross-Site Scripting (XSS)
-- _The most widespread, and one of the most devastating security vulnerabilities in web applications is XSS. This malicious attack injects client-side executable code. Rails provides helper methods to fend these attacks off._
-h5. Entry points
+h5. Entry Points
An entry point is a vulnerable URL and its parameters where an attacker can start an attack.
@@ -700,7 +700,7 @@ The most common entry points are message posts, user comments, and guest books,
XSS attacks work like this: An attacker injects some code, the web application saves it and displays it on a page, later presented to a victim. Most XSS examples simply display an alert box, but it is more powerful than that. XSS can steal the cookie, hijack the session, redirect the victim to a fake website, display advertisements for the benefit of the attacker, change elements on the web site to get confidential information or install malicious software through security holes in the web browser.
-During the second half of 2007, there were 88 vulnerabilities reported in Mozilla browsers, 22 in Safari, 18 in IE, and 12 in Opera. The "Symantec Global Internet Security threat report":http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf also documented 239 browser plug-in vulnerabilities in the last six months of 2007. "Mpack":http://pandalabs.pandasecurity.com/archive/MPack-uncovered_2100_.aspx is a very active and up-to-date attack framework which exploits these vulnerabilities. For criminal hackers, it is very attractive to exploit an SQL-Injection vulnerability in a web application framework and insert malicious code in every textual table column. In April 2008 more than 510,000 sites "were hacked":http://www.0x000000.com/?i=556 like this, among them the British government, United Nations, and many more high targets.
+During the second half of 2007, there were 88 vulnerabilities reported in Mozilla browsers, 22 in Safari, 18 in IE, and 12 in Opera. The "Symantec Global Internet Security threat report":http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf also documented 239 browser plug-in vulnerabilities in the last six months of 2007. "Mpack":http://pandalabs.pandasecurity.com/archive/MPack-uncovered_2100_.aspx is a very active and up-to-date attack framework which exploits these vulnerabilities. For criminal hackers, it is very attractive to exploit an SQL-Injection vulnerability in a web application framework and insert malicious code in every textual table column. In April 2008 more than 510,000 sites were hacked like this, among them the British government, United Nations, and many more high targets.
A relatively new, and unusual, form of entry points are banner advertisements. In earlier 2008, malicious code appeared in banner ads on popular sites, such as MySpace and Excite, according to "Trend Micro":http://blog.trendmicro.com/myspace-excite-and-blick-serve-up-malicious-banner-ads/.
@@ -721,7 +721,7 @@ This JavaScript code will simply display an alert box. The next examples do exac
<table background="javascript:alert('Hello')">
</html>
-h6. Cookie theft
+h6. Cookie Theft
These examples don't do any harm so far, so let's see how an attacker can steal the user's cookie (and thus hijack the user's session). In JavaScript you can use the document.cookie property to read and write the document's cookie. JavaScript enforces the same origin policy, that means a script from one domain cannot access cookies of another domain. The document.cookie property holds the cookie of the originating web server. However, you can read and write this property, if you embed the code directly in the HTML document (as it happens with XSS). Inject this anywhere in your web application to see your own cookie on the result page:
@@ -751,7 +751,7 @@ With web page defacement an attacker can do a lot of things, for example, presen
<iframe name=”StatPage” src="http://58.xx.xxx.xxx" width=5 height=5 style=”display:none”></iframe>
</html>
-This loads arbitrary HTML and/or JavaScript from an external source and embeds it as part of the site. This iFrame is taken from an "actual attack":http://www.symantec.com/enterprise/security_response/weblog/2007/06/italy_under_attack_mpack_gang.html on legitimate Italian sites using the "Mpack attack framework":http://isc.sans.org/diary.html?storyid=3015. Mpack tries to install malicious software through security holes in the web browser – very successfully, 50% of the attacks succeed.
+This loads arbitrary HTML and/or JavaScript from an external source and embeds it as part of the site. This iframe is taken from an actual attack on legitimate Italian sites using the "Mpack attack framework":http://isc.sans.org/diary.html?storyid=3015. Mpack tries to install malicious software through security holes in the web browser – very successfully, 50% of the attacks succeed.
A more specialized attack could overlap the entire web site or display a login form, which looks the same as the site's original, but transmits the user name and password to the attackers site. Or it could use CSS and/or JavaScript to hide a legitimate link in the web application, and display another one at its place which redirects to a fake web site.
@@ -796,7 +796,7 @@ Network traffic is mostly based on the limited Western alphabet, so new characte
This example pops up a message box. It will be recognized by the above sanitize() filter, though. A great tool to obfuscate and encode strings, and thus “get to know your enemy”, is the "Hackvertor":http://www.businessinfo.co.uk/labs/hackvertor/hackvertor.php. Rails‘ sanitize() method does a good job to fend off encoding attacks.
-h5. Examples from the underground
+h5. Examples from the Underground
_In order to understand today's attacks on web applications, it's best to take a look at some real-world attack vectors._
@@ -810,7 +810,7 @@ The following is an excerpt from the "Js.Yamanner@m":http://www.symantec.com/sec
The worms exploits a hole in Yahoo's HTML/JavaScript filter, which usually filters all target and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application.
-Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details and a video demonstration on "Rosario Valotta's website":http://rosario.valotta.googlepages.com/home. Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with.
+Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details on "Rosario Valotta's paper":http://www.xssed.com/article/9/Paper_A_PoC_of_a_cross_webmail_worm_XWW_called_Njuda_connection/. Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with.
In December 2006, 34,000 actual user names and passwords were stolen in a "MySpace phishing attack":http://news.netcraft.com/archives/2006/10/27/myspace_accounts_compromised_by_phishers.html. The idea of the attack was to create a profile page named “login_home_index_html”, so the URL looked very convincing. Specially-crafted HTML and CSS was used to hide the genuine MySpace content from the page and instead display its own login form.
@@ -858,11 +858,10 @@ This example, again, showed that a blacklist filter is never complete. However,
h4. Textile Injection
--- _If you want to provide text formatting other than HTML (due to security), use a mark-up language which is converted to HTML on the server-side. "RedCloth":http://whytheluckystiff.net/ruby/redcloth/ is such a language for Ruby, but without precautions, it is also vulnerable to XSS._
+-- _If you want to provide text formatting other than HTML (due to security), use a mark-up language which is converted to HTML on the server-side. "RedCloth":http://redcloth.org/ is such a language for Ruby, but without precautions, it is also vulnerable to XSS._
For example, RedCloth translates +_test_+ to &lt;em&gt;test&lt;em&gt;, which makes the text italic. However, up to the current version 3.0.4, it is still vulnerable to XSS. Get the "all-new version 4":http://www.redcloth.org that removed serious bugs. However, even that version has "some security bugs":http://www.rorsecurity.info/journal/2008/10/13/new-redcloth-security.html, so the countermeasures still apply. Here is an example for version 3.0.4:
-
<ruby>
RedCloth.new('<script>alert(1)</script>').to_html
# => "<script>alert(1)</script>"
@@ -971,7 +970,7 @@ Content-Type: text/html
Under certain circumstances this would present the malicious HTML to the victim. However, this seems to work with Keep-Alive connections, only (and many browsers are using one-time connections). But you can't rely on this. _(highlight)In any case this is a serious bug, and you should update your Rails to version 2.0.5 or 2.1.2 to eliminate Header Injection (and thus response splitting) risks._
-h3. Additional resources
+h3. Additional Resources
The security landscape shifts and it is important to keep up to date, because missing a new vulnerability can be catastrophic. You can find additional resources about (Rails) security here:
@@ -979,7 +978,6 @@ The security landscape shifts and it is important to keep up to date, because mi
* Subscribe to the Rails security "mailing list":http://groups.google.com/group/rubyonrails-security
* "Keep up to date on the other application layers":http://secunia.com/ (they have a weekly newsletter, too)
* A "good security blog":http://ha.ckers.org/blog/ including the "Cross-Site scripting Cheat Sheet":http://ha.ckers.org/xss.html
-* Another "good security blog":http://www.0x000000.com/ with some Cheat Sheets, too
h3. Changelog
diff --git a/railties/guides/source/testing.textile b/railties/guides/source/testing.textile
index 9b764806a8..12fc836edf 100644
--- a/railties/guides/source/testing.textile
+++ b/railties/guides/source/testing.textile
@@ -20,7 +20,7 @@ h3. Introduction to Testing
Testing support was woven into the Rails fabric from the beginning. It wasn't an "oh! let's bolt on support for running tests because they're new and cool" epiphany. Just about every Rails application interacts heavily with a database - and, as a result, your tests will need a database to interact with as well. To write efficient tests, you'll need to understand how to set up this database and populate it with sample data.
-h4. The 3 Environments
+h4. The Three Environments
Every Rails application you build has 3 sides: a side for production, a side for development, and a side for testing.
@@ -52,7 +52,7 @@ h4. The Low-Down on Fixtures
For good tests, you'll need to give some thought to setting up test data. In Rails, you can handle this by defining and customizing fixtures.
-h5. What Are Fixtures?
+h5. What are 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*. In this guide we will use *YAML* which is the preferred format.
@@ -110,9 +110,9 @@ h5. Fixtures in Action
Rails by default automatically loads all fixtures from the 'test/fixtures' folder for your unit and functional test. Loading involves three steps:
- * Remove any existing data from the table corresponding to the fixture
- * Load the fixture data into the table
- * Dump the fixture data into a variable in case you want to access it directly
+* Remove any existing data from the table corresponding to the fixture
+* Load the fixture data into the table
+* Dump the fixture data into a variable in case you want to access it directly
h5. Hashes with Special Powers
@@ -140,9 +140,9 @@ h3. Unit Testing your Models
In Rails, unit tests are what you write to test your models.
-For this guide we will be using Rails _scaffolding_. It will create the model, a migration, controller and views for the new resource in a single operation. It will also create a full test suite following Rails best practises. I will be using examples from this generated code and would be supplementing it with additional examples where necessary.
+For this guide we will be using Rails _scaffolding_. It will create the model, a migration, controller and views for the new resource in a single operation. It will also create a full test suite following Rails best practices. I will be using examples from this generated code and would be supplementing it with additional examples where necessary.
-NOTE: For more information on Rails _scaffolding_, refer to "Getting Started with Rails":../getting_started_with_rails.html
+NOTE: For more information on Rails _scaffolding_, refer to "Getting Started with Rails":getting_started.html
When you use +script/generate scaffold+, for a resource among other things it creates a test stub in the +test/unit+ folder:
@@ -396,7 +396,7 @@ There are a bunch of different types of assertions you can use. Here's the compl
|+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_raise( 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+.|
@@ -429,7 +429,7 @@ h3. Functional Tests for Your Controllers
In Rails, testing the various actions of a single controller is called writing functional tests for that controller. Controllers handle the incoming web requests to your application and eventually respond with a rendered view.
-h4. What to include in your Functional Tests
+h4. What to Include in your Functional Tests
You should test for things such as:
@@ -500,7 +500,7 @@ If you're familiar with the HTTP protocol, you'll know that +get+ is a type of r
All of request types are methods that you can use, however, you'll probably end up using the first two more often than the others.
-h4. The 4 Hashes of the Apocalypse
+h4. The Four Hashes of the Apocalypse
After a request has been made by using one of the 5 methods (+get+, +post+, etc.) and processed, you will have 4 Hash objects ready for use:
@@ -582,9 +582,9 @@ assert_select "ol" do
end
</ruby>
-The +assert_select+ assertion is quite powerful. For more advanced usage, refer to its "documentation":http://api.rubyonrails.com/classes/ActionController/Assertions/SelectorAssertions.html.
+The +assert_select+ assertion is quite powerful. For more advanced usage, refer to its "documentation":http://api.rubyonrails.org/classes/ActionController/Assertions/SelectorAssertions.html.
-h5. Additional View-based Assertions
+h5. Additional View-Based Assertions
There are more assertions that are primarily used in testing views:
@@ -631,7 +631,7 @@ end
Integration tests inherit from +ActionController::IntegrationTest+. This makes available some additional helpers to use in your integration tests. Also you need to explicitly include the fixtures to be made available to the test.
-h4. Helpers Available for Integration tests
+h4. Helpers Available for Integration Tests
In addition to the standard testing helpers, there are some additional helpers available to integration tests:
@@ -741,7 +741,7 @@ You don't need to set up and run your tests by hand on a test-by-test basis. Rai
|+rake test:uncommitted+ |Runs all the tests which are uncommitted. Only supports Subversion|
|+rake test:plugins+ |Run all the plugin tests from +vendor/plugins/*/**/test+ (or specify with +PLUGIN=_name_+)|
-h3. Brief Note About Test::Unit
+h3. Brief Note About +Test::Unit+
Ruby ships with a boat load of libraries. One little gem of a library is +Test::Unit+, a framework for unit testing in Ruby. All the basic assertions discussed above are actually defined in +Test::Unit::Assertions+. The class +ActiveSupport::TestCase+ which we have been using in our unit and functional tests extends +Test::Unit::TestCase+ that it is how we can use all the basic assertions in our tests.
@@ -872,7 +872,7 @@ For the purposes of unit testing a mailer, fixtures are used to provide an examp
When you generated your mailer, the generator creates stub fixtures for each of the mailers actions. If you didn't use the generator you'll have to make those files yourself.
-h5. The Basic Test case
+h5. The Basic Test Case
Here's a unit test to test a mailer named +UserMailer+ whose action +invite+ is used to send an invitation to a friend. It is an adapted version of the base test created by the generator for an +invite+ action.
diff --git a/railties/lib/commands/plugin.rb b/railties/lib/commands/plugin.rb
index 9ff4739562..3d76bcc73f 100644
--- a/railties/lib/commands/plugin.rb
+++ b/railties/lib/commands/plugin.rb
@@ -1,47 +1,8 @@
# Rails Plugin Manager.
-#
-# Listing available plugins:
-#
-# $ ./script/plugin list
-# continuous_builder http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder
-# asset_timestamping http://svn.aviditybytes.com/rails/plugins/asset_timestamping
-# enumerations_mixin http://svn.protocool.com/rails/plugins/enumerations_mixin/trunk
-# calculations http://techno-weenie.net/svn/projects/calculations/
-# ...
#
# Installing plugins:
#
# $ ./script/plugin install continuous_builder asset_timestamping
-#
-# Finding Repositories:
-#
-# $ ./script/plugin discover
-#
-# Adding Repositories:
-#
-# $ ./script/plugin source http://svn.protocool.com/rails/plugins/
-#
-# How it works:
-#
-# * Maintains a list of subversion repositories that are assumed to have
-# a plugin directory structure. Manage them with the (source, unsource,
-# and sources commands)
-#
-# * The discover command scrapes the following page for things that
-# look like subversion repositories with plugins:
-# http://wiki.rubyonrails.org/rails/pages/Plugins
-#
-# * Unless you specify that you want to use svn, script/plugin uses plain old
-# HTTP for downloads. The following bullets are true if you specify
-# that you want to use svn.
-#
-# * If `vendor/plugins` is under subversion control, the script will
-# modify the svn:externals property and perform an update. You can
-# use normal subversion commands to keep the plugins up to date.
-#
-# * Or, if `vendor/plugins` is not under subversion control, the
-# plugin is pulled via `svn checkout` or `svn export` but looks
-# exactly the same.
#
# Specifying revisions:
#
@@ -134,7 +95,8 @@ class RailsEnvironment
def externals
return [] unless use_externals?
ext = `svn propget svn:externals "#{root}/vendor/plugins"`
- ext.reject{ |line| line.strip == '' }.map do |line|
+ lines = ext.respond_to?(:lines) ? ext.lines : ext
+ lines.reject{ |line| line.strip == '' }.map do |line|
line.strip.split(/\s+/, 2)
end
end
@@ -155,13 +117,13 @@ end
class Plugin
attr_reader :name, :uri
- def initialize(uri, name=nil)
+ def initialize(uri, name = nil)
@uri = uri
guess_name(uri)
end
def self.find(name)
- name =~ /\// ? new(name) : Repositories.instance.find_plugin(name)
+ new(name)
end
def to_s
@@ -207,10 +169,13 @@ class Plugin
else
puts "Plugin doesn't exist: #{path}"
end
- # clean up svn:externals
- externals = rails_env.externals
- externals.reject!{|n,u| name == n or name == u}
- rails_env.externals = externals
+
+ if rails_env.use_externals?
+ # clean up svn:externals
+ externals = rails_env.externals
+ externals.reject!{|n,u| name == n or name == u}
+ rails_env.externals = externals
+ end
end
def info
@@ -278,8 +243,8 @@ class Plugin
base_cmd += " #{options[:revision]}" if options[:revision]
puts base_cmd if $verbose
if system(base_cmd)
- puts "removing: .git" if $verbose
- rm_rf ".git"
+ puts "removing: .git .gitignore" if $verbose
+ rm_rf %w(.git .gitignore)
else
rm_rf install_path
end
@@ -309,129 +274,6 @@ class Plugin
end
end
-class Repositories
- include Enumerable
-
- def initialize(cache_file = File.join(find_home, ".rails-plugin-sources"))
- @cache_file = File.expand_path(cache_file)
- load!
- end
-
- def each(&block)
- @repositories.each(&block)
- end
-
- def add(uri)
- unless find{|repo| repo.uri == uri }
- @repositories.push(Repository.new(uri)).last
- end
- end
-
- def remove(uri)
- @repositories.reject!{|repo| repo.uri == uri}
- end
-
- def exist?(uri)
- @repositories.detect{|repo| repo.uri == uri }
- end
-
- def all
- @repositories
- end
-
- def find_plugin(name)
- @repositories.each do |repo|
- repo.each do |plugin|
- return plugin if plugin.name == name
- end
- end
- return nil
- end
-
- def load!
- contents = File.exist?(@cache_file) ? File.read(@cache_file) : defaults
- contents = defaults if contents.empty?
- @repositories = contents.split(/\n/).reject do |line|
- line =~ /^\s*#/ or line =~ /^\s*$/
- end.map { |source| Repository.new(source.strip) }
- end
-
- def save
- File.open(@cache_file, 'w') do |f|
- each do |repo|
- f.write(repo.uri)
- f.write("\n")
- end
- end
- end
-
- def defaults
- <<-DEFAULTS
- http://dev.rubyonrails.com/svn/rails/plugins/
- DEFAULTS
- end
-
- def find_home
- ['HOME', 'USERPROFILE'].each do |homekey|
- return ENV[homekey] if ENV[homekey]
- end
- if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
- return "#{ENV['HOMEDRIVE']}:#{ENV['HOMEPATH']}"
- end
- begin
- File.expand_path("~")
- rescue StandardError => ex
- if File::ALT_SEPARATOR
- "C:/"
- else
- "/"
- end
- end
- end
-
- def self.instance
- @instance ||= Repositories.new
- end
-
- def self.each(&block)
- self.instance.each(&block)
- end
-end
-
-class Repository
- include Enumerable
- attr_reader :uri, :plugins
-
- def initialize(uri)
- @uri = uri.chomp('/') << "/"
- @plugins = nil
- end
-
- def plugins
- unless @plugins
- if $verbose
- puts "Discovering plugins in #{@uri}"
- puts index
- end
-
- @plugins = index.reject{ |line| line !~ /\/$/ }
- @plugins.map! { |name| Plugin.new(File.join(@uri, name), name) }
- end
-
- @plugins
- end
-
- def each(&block)
- plugins.each(&block)
- end
-
- private
- def index
- @index ||= RecursiveHTTPFetcher.new(@uri).ls
- end
-end
-
-
# load default environment and parse arguments
require 'optparse'
module Commands
@@ -471,14 +313,8 @@ module Commands
o.separator ""
o.separator "COMMANDS"
- o.separator " discover Discover plugin repositories."
- o.separator " list List available plugins."
o.separator " install Install plugin(s) from known repositories or URLs."
- o.separator " update Update installed plugins."
o.separator " remove Uninstall plugins."
- o.separator " source Add a plugin source repository."
- o.separator " unsource Remove a plugin repository."
- o.separator " sources List currently configured plugin repositories."
o.separator ""
o.separator "EXAMPLES"
@@ -490,20 +326,6 @@ module Commands
o.separator " #{@script_name} install git://github.com/SomeGuy/my_awesome_plugin.git\n"
o.separator " Install a plugin and add a svn:externals entry to vendor/plugins"
o.separator " #{@script_name} install -x continuous_builder\n"
- o.separator " List all available plugins:"
- o.separator " #{@script_name} list\n"
- o.separator " List plugins in the specified repository:"
- o.separator " #{@script_name} list --source=http://dev.rubyonrails.com/svn/rails/plugins/\n"
- o.separator " Discover and prompt to add new repositories:"
- o.separator " #{@script_name} discover\n"
- o.separator " Discover new repositories but just list them, don't add anything:"
- o.separator " #{@script_name} discover -l\n"
- o.separator " Add a new repository to the source list:"
- o.separator " #{@script_name} source http://dev.rubyonrails.com/svn/rails/plugins/\n"
- o.separator " Remove a repository from the source list:"
- o.separator " #{@script_name} unsource http://dev.rubyonrails.com/svn/rails/plugins/\n"
- o.separator " Show currently configured repositories:"
- o.separator " #{@script_name} sources\n"
end
end
@@ -512,7 +334,7 @@ module Commands
options.parse!(general)
command = general.shift
- if command =~ /^(list|discover|install|source|unsource|sources|remove|update|info)$/
+ if command =~ /^(install|remove)$/
command = Commands.const_get(command.capitalize).new(self)
command.parse!(sub)
else
@@ -534,218 +356,6 @@ module Commands
end
end
-
- class List
- def initialize(base_command)
- @base_command = base_command
- @sources = []
- @local = false
- @remote = true
- end
-
- def options
- OptionParser.new do |o|
- o.set_summary_indent(' ')
- o.banner = "Usage: #{@base_command.script_name} list [OPTIONS] [PATTERN]"
- o.define_head "List available plugins."
- o.separator ""
- o.separator "Options:"
- o.separator ""
- o.on( "-s", "--source=URL1,URL2", Array,
- "Use the specified plugin repositories.") {|sources| @sources = sources}
- o.on( "--local",
- "List locally installed plugins.") {|local| @local, @remote = local, false}
- o.on( "--remote",
- "List remotely available plugins. This is the default behavior",
- "unless --local is provided.") {|remote| @remote = remote}
- end
- end
-
- def parse!(args)
- options.order!(args)
- unless @sources.empty?
- @sources.map!{ |uri| Repository.new(uri) }
- else
- @sources = Repositories.instance.all
- end
- if @remote
- @sources.map{|r| r.plugins}.flatten.each do |plugin|
- if @local or !plugin.installed?
- puts plugin.to_s
- end
- end
- else
- cd "#{@base_command.environment.root}/vendor/plugins"
- Dir["*"].select{|p| File.directory?(p)}.each do |name|
- puts name
- end
- end
- end
- end
-
-
- class Sources
- def initialize(base_command)
- @base_command = base_command
- end
-
- def options
- OptionParser.new do |o|
- o.set_summary_indent(' ')
- o.banner = "Usage: #{@base_command.script_name} sources [OPTIONS] [PATTERN]"
- o.define_head "List configured plugin repositories."
- o.separator ""
- o.separator "Options:"
- o.separator ""
- o.on( "-c", "--check",
- "Report status of repository.") { |sources| @sources = sources}
- end
- end
-
- def parse!(args)
- options.parse!(args)
- Repositories.each do |repo|
- puts repo.uri
- end
- end
- end
-
-
- class Source
- def initialize(base_command)
- @base_command = base_command
- end
-
- def options
- OptionParser.new do |o|
- o.set_summary_indent(' ')
- o.banner = "Usage: #{@base_command.script_name} source REPOSITORY [REPOSITORY [REPOSITORY]...]"
- o.define_head "Add new repositories to the default search list."
- end
- end
-
- def parse!(args)
- options.parse!(args)
- count = 0
- args.each do |uri|
- if Repositories.instance.add(uri)
- puts "added: #{uri.ljust(50)}" if $verbose
- count += 1
- else
- puts "failed: #{uri.ljust(50)}"
- end
- end
- Repositories.instance.save
- puts "Added #{count} repositories."
- end
- end
-
-
- class Unsource
- def initialize(base_command)
- @base_command = base_command
- end
-
- def options
- OptionParser.new do |o|
- o.set_summary_indent(' ')
- o.banner = "Usage: #{@base_command.script_name} unsource URI [URI [URI]...]"
- o.define_head "Remove repositories from the default search list."
- o.separator ""
- o.on_tail("-h", "--help", "Show this help message.") { puts o; exit }
- end
- end
-
- def parse!(args)
- options.parse!(args)
- count = 0
- args.each do |uri|
- if Repositories.instance.remove(uri)
- count += 1
- puts "removed: #{uri.ljust(50)}"
- else
- puts "failed: #{uri.ljust(50)}"
- end
- end
- Repositories.instance.save
- puts "Removed #{count} repositories."
- end
- end
-
-
- class Discover
- def initialize(base_command)
- @base_command = base_command
- @list = false
- @prompt = true
- end
-
- def options
- OptionParser.new do |o|
- o.set_summary_indent(' ')
- o.banner = "Usage: #{@base_command.script_name} discover URI [URI [URI]...]"
- o.define_head "Discover repositories referenced on a page."
- o.separator ""
- o.separator "Options:"
- o.separator ""
- o.on( "-l", "--list",
- "List but don't prompt or add discovered repositories.") { |list| @list, @prompt = list, !@list }
- o.on( "-n", "--no-prompt",
- "Add all new repositories without prompting.") { |v| @prompt = !v }
- end
- end
-
- def parse!(args)
- options.parse!(args)
- args = ['http://wiki.rubyonrails.org/rails/pages/Plugins'] if args.empty?
- args.each do |uri|
- scrape(uri) do |repo_uri|
- catch(:next_uri) do
- if @prompt
- begin
- $stdout.print "Add #{repo_uri}? [Y/n] "
- throw :next_uri if $stdin.gets !~ /^y?$/i
- rescue Interrupt
- $stdout.puts
- exit 1
- end
- elsif @list
- puts repo_uri
- throw :next_uri
- end
- Repositories.instance.add(repo_uri)
- puts "discovered: #{repo_uri}" if $verbose or !@prompt
- end
- end
- end
- Repositories.instance.save
- end
-
- def scrape(uri)
- require 'open-uri'
- puts "Scraping #{uri}" if $verbose
- dupes = []
- content = open(uri).each do |line|
- begin
- if line =~ /<a[^>]*href=['"]([^'"]*)['"]/ || line =~ /(svn:\/\/[^<|\n]*)/
- uri = $1
- if uri =~ /^\w+:\/\// && uri =~ /\/plugins\// && uri !~ /\/browser\// && uri !~ /^http:\/\/wiki\.rubyonrails/ && uri !~ /http:\/\/instiki/
- uri = extract_repository_uri(uri)
- yield uri unless dupes.include?(uri) || Repositories.instance.exist?(uri)
- dupes << uri
- end
- end
- rescue
- puts "Problems scraping '#{uri}': #{$!.to_s}"
- end
- end
- end
-
- def extract_repository_uri(uri)
- uri.match(/(svn|https?):.*\/plugins\//i)[0]
- end
- end
-
class Install
def initialize(base_command)
@base_command = base_command
@@ -816,41 +426,6 @@ module Commands
end
end
- class Update
- def initialize(base_command)
- @base_command = base_command
- end
-
- def options
- OptionParser.new do |o|
- o.set_summary_indent(' ')
- o.banner = "Usage: #{@base_command.script_name} update [name [name]...]"
- o.on( "-r REVISION", "--revision REVISION",
- "Checks out the given revision from subversion.",
- "Ignored if subversion is not used.") { |v| @revision = v }
- o.define_head "Update plugins."
- end
- end
-
- def parse!(args)
- options.parse!(args)
- root = @base_command.environment.root
- cd root
- args = Dir["vendor/plugins/*"].map do |f|
- File.directory?("#{f}/.svn") ? File.basename(f) : nil
- end.compact if args.empty?
- cd "vendor/plugins"
- args.each do |name|
- if File.directory?(name)
- puts "Updating plugin: #{name}"
- system("svn #{$verbose ? '' : '-q'} up \"#{name}\" #{@revision ? "-r #{@revision}" : ''}")
- else
- puts "Plugin doesn't exist: #{name}"
- end
- end
- end
- end
-
class Remove
def initialize(base_command)
@base_command = base_command
diff --git a/railties/lib/console_app.rb b/railties/lib/console_app.rb
index d7cd57564f..d7d01d703f 100644
--- a/railties/lib/console_app.rb
+++ b/railties/lib/console_app.rb
@@ -24,6 +24,7 @@ end
#reloads the environment
def reload!
puts "Reloading..."
+ Dispatcher.cleanup_application
Dispatcher.reload_application
true
end
diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb
index a31ae9422e..a04405a7c2 100644
--- a/railties/lib/initializer.rb
+++ b/railties/lib/initializer.rb
@@ -176,6 +176,9 @@ module Rails
# the framework is now fully initialized
after_initialize
+ # Setup database middleware after initializers have run
+ initialize_database_middleware
+
# Prepare dispatcher callbacks and run 'prepare' callbacks
prepare_dispatcher
@@ -298,7 +301,9 @@ module Rails
end
def load_gems
- @configuration.gems.each { |gem| gem.load }
+ unless $gems_build_rake_task
+ @configuration.gems.each { |gem| gem.load }
+ end
end
def check_gem_dependencies
@@ -410,7 +415,18 @@ Run `rake gems:install` to install the missing gems.
if configuration.frameworks.include?(:active_record)
ActiveRecord::Base.configurations = configuration.database_configuration
ActiveRecord::Base.establish_connection
- configuration.middleware.use ActiveRecord::QueryCache
+ end
+ end
+
+ def initialize_database_middleware
+ if configuration.frameworks.include?(:active_record)
+ if ActionController::Base.session_store == ActiveRecord::SessionStore
+ configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::ConnectionAdapters::ConnectionManagement
+ configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::QueryCache
+ else
+ configuration.middleware.use ActiveRecord::ConnectionAdapters::ConnectionManagement
+ configuration.middleware.use ActiveRecord::QueryCache
+ end
end
end
@@ -545,6 +561,9 @@ Run `rake gems:install` to install the missing gems.
end
def initialize_metal
+ Rails::Rack::Metal.requested_metals = configuration.metals
+ Rails::Rack::Metal.metal_paths += plugin_loader.engine_metal_paths
+
configuration.middleware.insert_before(
:"ActionController::RewindableInput",
Rails::Rack::Metal, :if => Rails::Rack::Metal.metals.any?)
@@ -699,6 +718,11 @@ Run `rake gems:install` to install the missing gems.
@plugins = plugins.nil? ? nil : plugins.map { |p| p.to_sym }
end
+ # The list of metals to load. If this is set to <tt>nil</tt>, all metals will
+ # be loaded in alphabetical order. If this is set to <tt>[]</tt>, no metals will
+ # be loaded. Otherwise metals will be loaded in the order specified
+ attr_accessor :metals
+
# The path to the root of the plugins directory. By default, it is in
# <tt>vendor/plugins</tt>.
attr_accessor :plugin_paths
diff --git a/railties/lib/rails/backtrace_cleaner.rb b/railties/lib/rails/backtrace_cleaner.rb
index e1b422716d..923ed8b31d 100644
--- a/railties/lib/rails/backtrace_cleaner.rb
+++ b/railties/lib/rails/backtrace_cleaner.rb
@@ -2,15 +2,15 @@ module Rails
class BacktraceCleaner < ActiveSupport::BacktraceCleaner
ERB_METHOD_SIG = /:in `_run_erb_.*/
- VENDOR_DIRS = %w( vendor/gems vendor/rails )
+ RAILS_GEMS = %w( actionpack activerecord actionmailer activesupport activeresource rails )
+
+ VENDOR_DIRS = %w( vendor/rails )
SERVER_DIRS = %w( lib/mongrel bin/mongrel
lib/passenger bin/passenger-spawn-server
lib/rack )
RAILS_NOISE = %w( script/server )
RUBY_NOISE = %w( rubygems/custom_require benchmark.rb )
- GEMS_DIR = Gem.default_dir
-
ALL_NOISE = VENDOR_DIRS + SERVER_DIRS + RAILS_NOISE + RUBY_NOISE
def initialize
@@ -18,10 +18,25 @@ module Rails
add_filter { |line| line.sub("#{RAILS_ROOT}/", '') }
add_filter { |line| line.sub(ERB_METHOD_SIG, '') }
add_filter { |line| line.sub('./', '/') } # for tests
- add_filter { |line| line.sub(/(#{GEMS_DIR})\/gems\/([a-z]+)-([0-9.]+)\/(.*)/, '\2 (\3) \4')} # http://gist.github.com/30430
+
+ add_gem_filters
+
add_silencer { |line| ALL_NOISE.any? { |dir| line.include?(dir) } }
+ add_silencer { |line| RAILS_GEMS.any? { |gem| line =~ /^#{gem} / } }
add_silencer { |line| line =~ %r(vendor/plugins/[^\/]+/lib) }
end
+
+
+ private
+ def add_gem_filters
+ Gem.path.each do |path|
+ # http://gist.github.com/30430
+ add_filter { |line| line.sub(/(#{path})\/gems\/([a-z]+)-([0-9.]+)\/(.*)/, '\2 (\3) \4')}
+ end
+
+ vendor_gems_path = Rails::GemDependency.unpacked_path.sub("#{RAILS_ROOT}/",'')
+ add_filter { |line| line.sub(/(#{vendor_gems_path})\/([a-z]+)-([0-9.]+)\/(.*)/, '\2 (\3) [v] \4')}
+ end
end
# For installing the BacktraceCleaner in the test/unit
diff --git a/railties/lib/rails/gem_dependency.rb b/railties/lib/rails/gem_dependency.rb
index 5a07841be8..3062a77104 100644
--- a/railties/lib/rails/gem_dependency.rb
+++ b/railties/lib/rails/gem_dependency.rb
@@ -7,8 +7,8 @@ module Gem
end
module Rails
- class GemDependency
- attr_accessor :lib, :source
+ class GemDependency < Gem::Dependency
+ attr_accessor :lib, :source, :dep
def self.unpacked_path
@unpacked_path ||= File.join(RAILS_ROOT, 'vendor', 'gems')
@@ -29,18 +29,6 @@ module Rails
end
end
- def framework_gem?
- @@framework_gems.has_key?(name)
- end
-
- def vendor_rails?
- Gem.loaded_specs.has_key?(name) && Gem.loaded_specs[name].loaded_from.empty?
- end
-
- def vendor_gem?
- Gem.loaded_specs.has_key?(name) && Gem.loaded_specs[name].loaded_from.include?(self.class.unpacked_path)
- end
-
def initialize(name, options = {})
require 'rubygems' unless Object.const_defined?(:Gem)
@@ -52,10 +40,11 @@ module Rails
req = Gem::Requirement.default
end
- @dep = Gem::Dependency.new(name, req)
@lib = options[:lib]
@source = options[:source]
@loaded = @frozen = @load_paths_added = false
+
+ super(name, req)
end
def add_load_paths
@@ -65,7 +54,7 @@ module Rails
@load_paths_added = @loaded = @frozen = true
return
end
- gem @dep
+ gem self
@spec = Gem.loaded_specs[name]
@frozen = @spec.loaded_from.include?(self.class.unpacked_path) if @spec
@load_paths_added = true
@@ -74,44 +63,67 @@ module Rails
def dependencies
return [] if framework_gem?
- return [] if specification.nil?
- all_dependencies = specification.dependencies.map do |dependency|
+ return [] unless installed?
+ specification.dependencies.reject do |dependency|
+ dependency.type == :development
+ end.map do |dependency|
GemDependency.new(dependency.name, :requirement => dependency.version_requirements)
end
- all_dependencies += all_dependencies.map(&:dependencies).flatten
- all_dependencies.uniq
end
- def gem_dir(base_directory)
- File.join(base_directory, specification.full_name)
- end
-
- def spec_filename(base_directory)
- File.join(gem_dir(base_directory), '.specification')
+ def specification
+ # code repeated from Gem.activate. Find a matching spec, or the currently loaded version.
+ # error out if loaded version and requested version are incompatible.
+ @spec ||= begin
+ matches = Gem.source_index.search(self)
+ matches << @@framework_gems[name] if framework_gem?
+ if Gem.loaded_specs[name] then
+ # This gem is already loaded. If the currently loaded gem is not in the
+ # list of candidate gems, then we have a version conflict.
+ existing_spec = Gem.loaded_specs[name]
+ unless matches.any? { |spec| spec.version == existing_spec.version } then
+ raise Gem::Exception,
+ "can't activate #{@dep}, already activated #{existing_spec.full_name}"
+ end
+ # we're stuck with it, so change to match
+ version_requirements = Gem::Requirement.create("=#{existing_spec.version}")
+ existing_spec
+ else
+ # new load
+ matches.last
+ end
+ end
end
- def load
- return if @loaded || @load_paths_added == false
- require(@lib || name) unless @lib == false
- @loaded = true
- rescue LoadError
- puts $!.to_s
- $!.backtrace.each { |b| puts b }
+ def requirement
+ r = version_requirements
+ (r == Gem::Requirement.default) ? nil : r
end
- def name
- @dep.name.to_s
+ def built?
+ # TODO: If Rubygems ever gives us a way to detect this, we should use it
+ false
end
- def requirement
- r = @dep.version_requirements
- (r == Gem::Requirement.default) ? nil : r
+ def framework_gem?
+ @@framework_gems.has_key?(name)
end
def frozen?
@frozen ||= vendor_rails? || vendor_gem?
end
+ def installed?
+ Gem.loaded_specs.keys.include?(name)
+ end
+
+ def load_paths_added?
+ # always try to add load paths - even if a gem is loaded, it may not
+ # be a compatible version (ie random_gem 0.4 is loaded and a later spec
+ # needs >= 0.5 - gem 'random_gem' will catch this and error out)
+ @load_paths_added
+ end
+
def loaded?
@loaded ||= begin
if vendor_rails?
@@ -135,46 +147,49 @@ module Rails
end
end
- def load_paths_added?
- # always try to add load paths - even if a gem is loaded, it may not
- # be a compatible version (ie random_gem 0.4 is loaded and a later spec
- # needs >= 0.5 - gem 'random_gem' will catch this and error out)
- @load_paths_added
+ def vendor_rails?
+ Gem.loaded_specs.has_key?(name) && Gem.loaded_specs[name].loaded_from.empty?
end
- def install
- cmd = "#{gem_command} #{install_command.join(' ')}"
- puts cmd
- puts %x(#{cmd})
+ def vendor_gem?
+ specification && File.exists?(unpacked_gem_directory)
end
- def unpack_to(directory)
- FileUtils.mkdir_p directory
- Dir.chdir directory do
- Gem::GemRunner.new.run(unpack_command)
+ def build
+ require 'rails/gem_builder'
+ unless built?
+ return unless File.exists?(unpacked_specification_filename)
+ spec = YAML::load_file(unpacked_specification_filename)
+ Rails::GemBuilder.new(spec, unpacked_gem_directory).build_extensions
+ puts "Built gem: '#{unpacked_gem_directory}'"
end
-
- # Gem.activate changes the spec - get the original
- real_spec = Gem::Specification.load(specification.loaded_from)
- write_spec(directory, real_spec)
-
+ dependencies.each { |dep| dep.build }
end
- def write_spec(directory, spec)
- # copy the gem's specification into GEMDIR/.specification so that
- # we can access information about the gem on deployment systems
- # without having the gem installed
- File.open(spec_filename(directory), 'w') do |file|
- file.puts spec.to_yaml
+ def install
+ unless installed?
+ cmd = "#{gem_command} #{install_command.join(' ')}"
+ puts cmd
+ puts %x(#{cmd})
end
end
- def refresh_spec(directory)
+ def load
+ return if @loaded || @load_paths_added == false
+ require(@lib || name) unless @lib == false
+ @loaded = true
+ rescue LoadError
+ puts $!.to_s
+ $!.backtrace.each { |b| puts b }
+ end
+
+ def refresh
+ Rails::VendorGemSourceIndex.silence_spec_warnings = true
real_gems = Gem.source_index.installed_source_index
exact_dep = Gem::Dependency.new(name, "= #{specification.version}")
matches = real_gems.search(exact_dep)
installed_spec = matches.first
- if File.exist?(File.dirname(spec_filename(directory)))
+ if frozen?
if installed_spec
# we have a real copy
# get a fresh spec - matches should only have one element
@@ -182,11 +197,11 @@ module Rails
# spec is the same as the copy from real_gems - Gem.activate changes
# some of the fields
real_spec = Gem::Specification.load(matches.first.loaded_from)
- write_spec(directory, real_spec)
+ write_specification(real_spec)
puts "Reloaded specification for #{name} from installed gems."
else
# the gem isn't installed locally - write out our current specs
- write_spec(directory, specification)
+ write_specification(specification)
puts "Gem #{name} not loaded locally - writing out current spec."
end
else
@@ -198,42 +213,44 @@ module Rails
end
end
- def ==(other)
- self.name == other.name && self.requirement == other.requirement
+ def unpack(options={})
+ unless frozen? || framework_gem?
+ FileUtils.mkdir_p unpack_base
+ Dir.chdir unpack_base do
+ Gem::GemRunner.new.run(unpack_command)
+ end
+ # Gem.activate changes the spec - get the original
+ real_spec = Gem::Specification.load(specification.loaded_from)
+ write_specification(real_spec)
+ end
+ dependencies.each { |dep| dep.unpack } if options[:recursive]
end
- alias_method :"eql?", :"=="
- def hash
- @dep.hash
+ def write_specification(spec)
+ # copy the gem's specification into GEMDIR/.specification so that
+ # we can access information about the gem on deployment systems
+ # without having the gem installed
+ File.open(unpacked_specification_filename, 'w') do |file|
+ file.puts spec.to_yaml
+ end
end
- def specification
- # code repeated from Gem.activate. Find a matching spec, or the currently loaded version.
- # error out if loaded version and requested version are incompatible.
- @spec ||= begin
- matches = Gem.source_index.search(@dep)
- matches << @@framework_gems[name] if framework_gem?
- if Gem.loaded_specs[name] then
- # This gem is already loaded. If the currently loaded gem is not in the
- # list of candidate gems, then we have a version conflict.
- existing_spec = Gem.loaded_specs[name]
- unless matches.any? { |spec| spec.version == existing_spec.version } then
- raise Gem::Exception,
- "can't activate #{@dep}, already activated #{existing_spec.full_name}"
- end
- # we're stuck with it, so change to match
- @dep.version_requirements = Gem::Requirement.create("=#{existing_spec.version}")
- existing_spec
- else
- # new load
- matches.last
- end
- end
+ def ==(other)
+ self.name == other.name && self.requirement == other.requirement
end
+ alias_method :"eql?", :"=="
private
+
def gem_command
- RUBY_PLATFORM =~ /win32/ ? 'gem.bat' : 'gem'
+ case RUBY_PLATFORM
+ when /win32/
+ 'gem.bat'
+ when /java/
+ 'jruby -S gem'
+ else
+ 'gem'
+ end
end
def install_command
@@ -248,5 +265,18 @@ module Rails
cmd << "--version" << "= "+specification.version.to_s if requirement
cmd
end
+
+ def unpack_base
+ Rails::GemDependency.unpacked_path
+ end
+
+ def unpacked_gem_directory
+ File.join(unpack_base, specification.full_name)
+ end
+
+ def unpacked_specification_filename
+ File.join(unpacked_gem_directory, '.specification')
+ end
+
end
end
diff --git a/railties/lib/rails/plugin.rb b/railties/lib/rails/plugin.rb
index 4901abe808..80deb73bbb 100644
--- a/railties/lib/rails/plugin.rb
+++ b/railties/lib/rails/plugin.rb
@@ -80,6 +80,10 @@ module Rails
File.join(directory, 'app', 'controllers')
end
+ def metal_path
+ File.join(directory, 'app', 'metal')
+ end
+
def routing_file
File.join(directory, 'config', 'routes.rb')
end
@@ -100,7 +104,7 @@ module Rails
def app_paths
- [ File.join(directory, 'app', 'models'), File.join(directory, 'app', 'helpers'), controller_path ]
+ [ File.join(directory, 'app', 'models'), File.join(directory, 'app', 'helpers'), controller_path, metal_path ]
end
def lib_path
@@ -160,4 +164,4 @@ module Rails
File.join(directory, 'rails', 'init.rb')
end
end
-end \ No newline at end of file
+end
diff --git a/railties/lib/rails/plugin/loader.rb b/railties/lib/rails/plugin/loader.rb
index 7f85bb8966..66e01d70da 100644
--- a/railties/lib/rails/plugin/loader.rb
+++ b/railties/lib/rails/plugin/loader.rb
@@ -65,6 +65,9 @@ module Rails
$LOAD_PATH.uniq!
end
+ def engine_metal_paths
+ engines.collect(&:metal_path)
+ end
protected
def configure_engines
@@ -178,11 +181,11 @@ module Rails
if explicit_plugin_loading_order?
if configuration.plugins.detect {|plugin| plugin != :all && !loaded?(plugin) }
missing_plugins = configuration.plugins - (plugins.map{|p| p.name.to_sym} + [:all])
- raise LoadError, "Could not locate the following plugins: #{missing_plugins.to_sentence}"
+ raise LoadError, "Could not locate the following plugins: #{missing_plugins.to_sentence(:locale => :en)}"
end
end
end
end
end
-end \ No newline at end of file
+end
diff --git a/railties/lib/rails/rack/metal.rb b/railties/lib/rails/rack/metal.rb
index b185227234..adc43da864 100644
--- a/railties/lib/rails/rack/metal.rb
+++ b/railties/lib/rails/rack/metal.rb
@@ -6,15 +6,30 @@ module Rails
NotFoundResponse = [404, {}, []].freeze
NotFound = lambda { NotFoundResponse }
+ cattr_accessor :metal_paths
+ self.metal_paths = ["#{Rails.root}/app/metal"]
+ cattr_accessor :requested_metals
+
def self.metals
- base = "#{Rails.root}/app/metal"
- matcher = /\A#{Regexp.escape(base)}\/(.*)\.rb\Z/
+ matcher = /#{Regexp.escape('/app/metal/')}(.*)\.rb\Z/
+ metal_glob = metal_paths.map{ |base| "#{base}/**/*.rb" }
+ all_metals = {}
- Dir["#{base}/**/*.rb"].sort.map do |file|
- file.sub!(matcher, '\1')
- require file
- file.classify.constantize
+ metal_glob.each do |glob|
+ Dir[glob].sort.map do |file|
+ file = file.match(matcher)[1]
+ all_metals[file.camelize] = file
+ end
end
+
+ load_list = requested_metals || all_metals.keys
+
+ load_list.map do |requested_metal|
+ if metal = all_metals[requested_metal]
+ require metal
+ requested_metal.constantize
+ end
+ end.compact
end
def initialize(app)
diff --git a/railties/lib/rails/rack/static.rb b/railties/lib/rails/rack/static.rb
index ef4e2642e2..f07c6beb5e 100644
--- a/railties/lib/rails/rack/static.rb
+++ b/railties/lib/rails/rack/static.rb
@@ -13,14 +13,18 @@ module Rails
def call(env)
path = env['PATH_INFO'].chomp('/')
method = env['REQUEST_METHOD']
- cached_path = (path.empty? ? 'index' : path) + ::ActionController::Base.page_cache_extension
if FILE_METHODS.include?(method)
if file_exist?(path)
return @file_server.call(env)
- elsif file_exist?(cached_path)
- env['PATH_INFO'] = cached_path
- return @file_server.call(env)
+ else
+ cached_path = directory_exist?(path) ? "#{path}/index" : path
+ cached_path += ::ActionController::Base.page_cache_extension
+
+ if file_exist?(cached_path)
+ env['PATH_INFO'] = cached_path
+ return @file_server.call(env)
+ end
end
end
@@ -32,6 +36,11 @@ module Rails
full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path))
File.file?(full_path) && File.readable?(full_path)
end
+
+ def directory_exist?(path)
+ full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path))
+ File.directory?(full_path) && File.readable?(full_path)
+ end
end
end
end
diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb
index 9bb4b2a96d..99c7516a65 100644
--- a/railties/lib/rails/version.rb
+++ b/railties/lib/rails/version.rb
@@ -2,7 +2,7 @@ module Rails
module VERSION #:nodoc:
MAJOR = 2
MINOR = 3
- TINY = 0
+ TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end
diff --git a/railties/lib/rails_generator/commands.rb b/railties/lib/rails_generator/commands.rb
index 299044c3d7..b684dc92be 100644
--- a/railties/lib/rails_generator/commands.rb
+++ b/railties/lib/rails_generator/commands.rb
@@ -182,15 +182,19 @@ HELP
nesting = class_name.split('::')
name = nesting.pop
+ # Hack to limit const_defined? to non-inherited on 1.9.
+ extra = []
+ extra << false unless Object.method(:const_defined?).arity == 1
+
# Extract the last Module in the nesting.
last = nesting.inject(Object) { |last, nest|
- break unless last.const_defined?(nest)
+ break unless last.const_defined?(nest, *extra)
last.const_get(nest)
}
# If the last Module exists, check whether the given
# class exists and raise a collision if so.
- if last and last.const_defined?(name.camelize)
+ if last and last.const_defined?(name.camelize, *extra)
raise_class_collision(class_name)
end
end
diff --git a/railties/lib/rails_generator/generators/applications/app/template_runner.rb b/railties/lib/rails_generator/generators/applications/app/template_runner.rb
index eeb6b17661..3b49b1fa92 100644
--- a/railties/lib/rails_generator/generators/applications/app/template_runner.rb
+++ b/railties/lib/rails_generator/generators/applications/app/template_runner.rb
@@ -85,26 +85,35 @@ module Rails
# Adds an entry into config/environment.rb for the supplied gem :
def gem(name, options = {})
log 'gem', name
+ env = options.delete(:env)
gems_code = "config.gem '#{name}'"
if options.any?
- opts = options.inject([]) {|result, h| result << [":#{h[0]} => '#{h[1]}'"] }.sort.join(", ")
+ opts = options.inject([]) {|result, h| result << [":#{h[0]} => #{h[1].inspect.gsub('"',"'")}"] }.sort.join(", ")
gems_code << ", #{opts}"
end
- environment gems_code
+ environment gems_code, :env => env
end
# Adds a line inside the Initializer block for config/environment.rb. Used by #gem
- def environment(data = nil, &block)
+ # If options :env is specified, the line is appended to the corresponding
+ # file in config/environments/#{env}.rb
+ def environment(data = nil, options = {}, &block)
sentinel = 'Rails::Initializer.run do |config|'
data = block.call if !data && block_given?
in_root do
- gsub_file 'config/environment.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
- "#{match}\n " << data
+ if options[:env].nil?
+ gsub_file 'config/environment.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
+ "#{match}\n " << data
+ end
+ else
+ Array.wrap(options[:env]).each do|env|
+ append_file "config/environments/#{env}.rb", "\n#{data}"
+ end
end
end
end
@@ -307,7 +316,7 @@ module Rails
#
def ask(string)
log '', string
- gets.strip
+ STDIN.gets.strip
end
# Do something in the root of the Rails application or
@@ -356,6 +365,17 @@ module Rails
File.open(path, 'wb') { |file| file.write(content) }
end
+ # Append text to a file
+ #
+ # ==== Example
+ #
+ # append_file 'config/environments/test.rb', 'config.gem "rspec"'
+ #
+ def append_file(relative_destination, data)
+ path = destination_path(relative_destination)
+ File.open(path, 'ab') { |file| file.write(data) }
+ end
+
def destination_path(relative_destination)
File.join(root, relative_destination)
end
diff --git a/railties/lib/tasks/gems.rake b/railties/lib/tasks/gems.rake
index d538e52ca6..ed07bf2016 100644
--- a/railties/lib/tasks/gems.rake
+++ b/railties/lib/tasks/gems.rake
@@ -9,72 +9,57 @@ task :gems => 'gems:base' do
puts "R = Framework (loaded before rails starts)"
end
-def print_gem_status(gem, indent=1)
- code = gem.loaded? ? (gem.frozen? ? (gem.framework_gem? ? "R" : "F") : "I") : " "
- puts " "*(indent-1)+" - [#{code}] #{gem.name} #{gem.requirement.to_s}"
- gem.dependencies.each { |g| print_gem_status(g, indent+1)} if gem.loaded?
-end
-
namespace :gems do
task :base do
$gems_rake_task = true
+ require 'rubygems'
+ require 'rubygems/gem_runner'
Rake::Task[:environment].invoke
end
desc "Build any native extensions for unpacked gems"
task :build do
- $gems_rake_task = true
- require 'rails/gem_builder'
- Dir[File.join(Rails::GemDependency.unpacked_path, '*')].each do |gem_dir|
- spec_file = File.join(gem_dir, '.specification')
- next unless File.exists?(spec_file)
- specification = YAML::load_file(spec_file)
- next unless ENV['GEM'].blank? || ENV['GEM'] == specification.name
- Rails::GemBuilder.new(specification, gem_dir).build_extensions
- puts "Built gem: '#{gem_dir}'"
- end
+ $gems_build_rake_task = true
+ Rake::Task['gems:unpack'].invoke
+ current_gems.each &:build
end
- desc "Installs all required gems for this application."
+ desc "Installs all required gems."
task :install => :base do
- require 'rubygems'
- require 'rubygems/gem_runner'
- Rails.configuration.gems.each { |gem| gem.install unless gem.loaded? }
+ current_gems.each &:install
end
- desc "Unpacks the specified gem into vendor/gems."
- task :unpack => :base do
- require 'rubygems'
- require 'rubygems/gem_runner'
- Rails.configuration.gems.each do |gem|
- next unless !gem.frozen? && (ENV['GEM'].blank? || ENV['GEM'] == gem.name)
- gem.unpack_to(Rails::GemDependency.unpacked_path) if gem.loaded?
- end
+ desc "Unpacks all required gems into vendor/gems."
+ task :unpack => :install do
+ current_gems.each &:unpack
end
namespace :unpack do
- desc "Unpacks the specified gems and its dependencies into vendor/gems"
- task :dependencies => :unpack do
- require 'rubygems'
- require 'rubygems/gem_runner'
- Rails.configuration.gems.each do |gem|
- next unless ENV['GEM'].blank? || ENV['GEM'] == gem.name
- gem.dependencies.each do |dependency|
- next if dependency.frozen?
- dependency.unpack_to(Rails::GemDependency.unpacked_path)
- end
- end
+ desc "Unpacks all required gems and their dependencies into vendor/gems."
+ task :dependencies => :install do
+ current_gems.each { |gem| gem.unpack(:recursive => true) }
end
end
desc "Regenerate gem specifications in correct format."
task :refresh_specs => :base do
- require 'rubygems'
- require 'rubygems/gem_runner'
- Rails::VendorGemSourceIndex.silence_spec_warnings = true
- Rails.configuration.gems.each do |gem|
- next unless gem.frozen? && (ENV['GEM'].blank? || ENV['GEM'] == gem.name)
- gem.refresh_spec(Rails::GemDependency.unpacked_path) if gem.loaded?
- end
+ current_gems.each &:refresh
+ end
+end
+
+def current_gems
+ gems = Rails.configuration.gems
+ gems = gems.select { |gem| gem.name == ENV['GEM'] } unless ENV['GEM'].blank?
+ gems
+end
+
+def print_gem_status(gem, indent=1)
+ code = case
+ when gem.framework_gem? then 'R'
+ when gem.frozen? then 'F'
+ when gem.installed? then 'I'
+ else ' '
end
-end \ No newline at end of file
+ puts " "*(indent-1)+" - [#{code}] #{gem.name} #{gem.requirement.to_s}"
+ gem.dependencies.each { |g| print_gem_status(g, indent+1) }
+end
diff --git a/railties/lib/tasks/testing.rake b/railties/lib/tasks/testing.rake
index 4242458672..fd5e52a05b 100644
--- a/railties/lib/tasks/testing.rake
+++ b/railties/lib/tasks/testing.rake
@@ -48,7 +48,7 @@ task :test do
task
end
end.compact
- abort "Errors running #{errors.to_sentence}!" if errors.any?
+ abort "Errors running #{errors.to_sentence(:locale => :en)}!" if errors.any?
end
namespace :test do
diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb
index a950fc8b7e..0addcb8bf3 100644
--- a/railties/test/abstract_unit.rb
+++ b/railties/test/abstract_unit.rb
@@ -1,5 +1,7 @@
$:.unshift File.dirname(__FILE__) + "/../../activesupport/lib"
+$:.unshift File.dirname(__FILE__) + "/../../activerecord/lib"
$:.unshift File.dirname(__FILE__) + "/../../actionpack/lib"
+$:.unshift File.dirname(__FILE__) + "/../../actionmailer/lib"
$:.unshift File.dirname(__FILE__) + "/../lib"
$:.unshift File.dirname(__FILE__) + "/../builtin/rails_info"
diff --git a/railties/test/backtrace_cleaner_test.rb b/railties/test/backtrace_cleaner_test.rb
index 5955fd2856..7a1b361440 100644
--- a/railties/test/backtrace_cleaner_test.rb
+++ b/railties/test/backtrace_cleaner_test.rb
@@ -3,26 +3,59 @@ require 'abstract_unit'
require 'initializer'
require 'rails/backtrace_cleaner'
-class TestWithBacktrace
- include Test::Unit::Util::BacktraceFilter
- include Rails::BacktraceFilterForTestUnit
+if defined? Test::Unit::Util::BacktraceFilter
+ class TestWithBacktrace
+ include Test::Unit::Util::BacktraceFilter
+ include Rails::BacktraceFilterForTestUnit
+ end
+
+ class BacktraceCleanerFilterTest < ActiveSupport::TestCase
+ def setup
+ @test = TestWithBacktrace.new
+ @backtrace = [ './test/rails/benchmark_test.rb', './test/rails/dependencies.rb', '/opt/local/lib/ruby/kernel.rb' ]
+ end
+
+ test "test with backtrace should use the rails backtrace cleaner to clean" do
+ Rails.stubs(:backtrace_cleaner).returns(stub(:clean))
+ Rails.backtrace_cleaner.expects(:clean).with(@backtrace, nil)
+ @test.filter_backtrace(@backtrace)
+ end
+
+ test "filter backtrace should have the same arity as Test::Unit::Util::BacktraceFilter" do
+ assert_nothing_raised do
+ @test.filter_backtrace(@backtrace, '/opt/local/lib')
+ end
+ end
+ end
+else
+ $stderr.puts 'No BacktraceFilter for minitest'
end
-class BacktraceCleanerFilterTest < ActiveSupport::TestCase
+class BacktraceCleanerVendorGemTest < ActiveSupport::TestCase
def setup
- @test = TestWithBacktrace.new
- @backtrace = [ './test/rails/benchmark_test.rb', './test/rails/dependencies.rb', '/opt/local/lib/ruby/kernel.rb' ]
+ @cleaner = Rails::BacktraceCleaner.new
end
-
- test "test with backtrace should use the rails backtrace cleaner to clean" do
- Rails.stubs(:backtrace_cleaner).returns(stub(:clean))
- Rails.backtrace_cleaner.expects(:clean).with(@backtrace, nil)
- @test.filter_backtrace(@backtrace)
+
+ test "should format installed gems correctly" do
+ @backtrace = [ "#{Gem.default_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ]
+ @result = @cleaner.clean(@backtrace)
+ assert_equal "nosuchgem (1.2.3) lib/foo.rb", @result[0]
end
-
- test "filter backtrace should have the same arity as Test::Unit::Util::BacktraceFilter" do
- assert_nothing_raised do
- @test.filter_backtrace(@backtrace, '/opt/local/lib')
+
+ test "should format installed gems not in Gem.default_dir correctly" do
+ @target_dir = Gem.path.detect { |p| p != Gem.default_dir }
+ # skip this test if default_dir is the only directory on Gem.path
+ if @target_dir
+ @backtrace = [ "#{@target_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ]
+ @result = @cleaner.clean(@backtrace)
+ assert_equal "nosuchgem (1.2.3) lib/foo.rb", @result[0]
end
end
-end \ No newline at end of file
+
+ test "should format vendor gems correctly" do
+ @backtrace = [ "#{Rails::GemDependency.unpacked_path}/nosuchgem-1.2.3/lib/foo.rb" ]
+ @result = @cleaner.clean(@backtrace)
+ assert_equal "nosuchgem (1.2.3) [v] lib/foo.rb", @result[0]
+ end
+
+end
diff --git a/railties/test/boot_test.rb b/railties/test/boot_test.rb
index 16776af098..08fcc82e6f 100644
--- a/railties/test/boot_test.rb
+++ b/railties/test/boot_test.rb
@@ -62,6 +62,8 @@ class VendorBootTest < Test::Unit::TestCase
def test_load_initializer_requires_from_vendor_rails
boot = VendorBoot.new
boot.expects(:require).with("#{RAILS_ROOT}/vendor/rails/railties/lib/initializer")
+ Rails::Initializer.expects(:run).with(:install_gem_spec_stubs)
+ Rails::GemDependency.expects(:add_frozen_gem_path)
boot.load_initializer
end
end
diff --git a/railties/test/console_app_test.rb b/railties/test/console_app_test.rb
index 7a5de5af8f..f11de087e3 100644
--- a/railties/test/console_app_test.rb
+++ b/railties/test/console_app_test.rb
@@ -11,30 +11,32 @@ require 'dispatcher'
require 'console_app'
# console_app sets Test::Unit.run to work around the at_exit hook in test/unit, which kills IRB
-Test::Unit.run = false
-
-class ConsoleAppTest < Test::Unit::TestCase
- def test_app_method_should_return_integration_session
- assert_nothing_thrown do
- console_session = app
- assert_not_nil console_session
- assert_instance_of ActionController::Integration::Session,
- console_session
+if Test::Unit.respond_to?(:run=)
+ Test::Unit.run = false
+
+ class ConsoleAppTest < Test::Unit::TestCase
+ def test_app_method_should_return_integration_session
+ assert_nothing_thrown do
+ console_session = app
+ assert_not_nil console_session
+ assert_instance_of ActionController::Integration::Session,
+ console_session
+ end
end
- end
- def test_reload_should_fire_preparation_callbacks
- a = b = c = nil
+ def test_reload_should_fire_preparation_callbacks
+ a = b = c = nil
- Dispatcher.to_prepare { a = b = c = 1 }
- Dispatcher.to_prepare { b = c = 2 }
- Dispatcher.to_prepare { c = 3 }
- ActionController::Routing::Routes.expects(:reload)
+ Dispatcher.to_prepare { a = b = c = 1 }
+ Dispatcher.to_prepare { b = c = 2 }
+ Dispatcher.to_prepare { c = 3 }
+ ActionController::Routing::Routes.expects(:reload)
- reload!
+ reload!
- assert_equal 1, a
- assert_equal 2, b
- assert_equal 3, c
+ assert_equal 1, a
+ assert_equal 2, b
+ assert_equal 3, c
+ end
end
end
diff --git a/railties/test/fixtures/metal/multiplemetals/app/metal/metal_a.rb b/railties/test/fixtures/metal/multiplemetals/app/metal/metal_a.rb
new file mode 100644
index 0000000000..2d373ce422
--- /dev/null
+++ b/railties/test/fixtures/metal/multiplemetals/app/metal/metal_a.rb
@@ -0,0 +1,5 @@
+class MetalA < Rails::Rack::Metal
+ def self.call(env)
+ [200, { "Content-Type" => "text/html"}, ["Hi"]]
+ end
+end
diff --git a/railties/test/fixtures/metal/multiplemetals/app/metal/metal_b.rb b/railties/test/fixtures/metal/multiplemetals/app/metal/metal_b.rb
new file mode 100644
index 0000000000..a8bbf3fd60
--- /dev/null
+++ b/railties/test/fixtures/metal/multiplemetals/app/metal/metal_b.rb
@@ -0,0 +1,5 @@
+class MetalB < Rails::Rack::Metal
+ def self.call(env)
+ [200, { "Content-Type" => "text/html"}, ["Hi"]]
+ end
+end
diff --git a/railties/test/fixtures/metal/pluralmetal/app/metal/legacy_routes.rb b/railties/test/fixtures/metal/pluralmetal/app/metal/legacy_routes.rb
new file mode 100644
index 0000000000..0cd3737c32
--- /dev/null
+++ b/railties/test/fixtures/metal/pluralmetal/app/metal/legacy_routes.rb
@@ -0,0 +1,5 @@
+class LegacyRoutes < Rails::Rack::Metal
+ def self.call(env)
+ [301, { "Location" => "http://example.com"}, []]
+ end
+end
diff --git a/railties/test/fixtures/metal/singlemetal/app/metal/foo_metal.rb b/railties/test/fixtures/metal/singlemetal/app/metal/foo_metal.rb
new file mode 100644
index 0000000000..5f5b087592
--- /dev/null
+++ b/railties/test/fixtures/metal/singlemetal/app/metal/foo_metal.rb
@@ -0,0 +1,5 @@
+class FooMetal < Rails::Rack::Metal
+ def self.call(env)
+ [200, { "Content-Type" => "text/html"}, ["Hi"]]
+ end
+end
diff --git a/railties/test/fixtures/metal/subfolders/app/metal/Folder/metal_a.rb b/railties/test/fixtures/metal/subfolders/app/metal/Folder/metal_a.rb
new file mode 100644
index 0000000000..25b3bb0abc
--- /dev/null
+++ b/railties/test/fixtures/metal/subfolders/app/metal/Folder/metal_a.rb
@@ -0,0 +1,7 @@
+module Folder
+ class MetalA < Rails::Rack::Metal
+ def self.call(env)
+ [200, { "Content-Type" => "text/html"}, ["Hi"]]
+ end
+ end
+end
diff --git a/railties/test/fixtures/metal/subfolders/app/metal/Folder/metal_b.rb b/railties/test/fixtures/metal/subfolders/app/metal/Folder/metal_b.rb
new file mode 100644
index 0000000000..7583363f71
--- /dev/null
+++ b/railties/test/fixtures/metal/subfolders/app/metal/Folder/metal_b.rb
@@ -0,0 +1,7 @@
+module Folder
+ class MetalB < Rails::Rack::Metal
+ def self.call(env)
+ [200, { "Content-Type" => "text/html"}, ["Hi"]]
+ end
+ end
+end
diff --git a/railties/test/fixtures/plugins/engines/engine/app/metal/engine_metal.rb b/railties/test/fixtures/plugins/engines/engine/app/metal/engine_metal.rb
new file mode 100644
index 0000000000..d67a127ca7
--- /dev/null
+++ b/railties/test/fixtures/plugins/engines/engine/app/metal/engine_metal.rb
@@ -0,0 +1,10 @@
+class EngineMetal
+ def self.call(env)
+ if env["PATH_INFO"] =~ /^\/metal/
+ [200, {"Content-Type" => "text/html"}, ["Engine metal"]]
+ else
+ [404, {"Content-Type" => "text/html"}, ["Not Found"]]
+ end
+ end
+end
+
diff --git a/railties/test/fixtures/plugins/engines/engine/init.rb b/railties/test/fixtures/plugins/engines/engine/init.rb
index f4b00c0fa4..64e9ae6c30 100644
--- a/railties/test/fixtures/plugins/engines/engine/init.rb
+++ b/railties/test/fixtures/plugins/engines/engine/init.rb
@@ -1,3 +1,3 @@
# My app/models dir must be in the load path.
require 'engine_model'
-raise 'missing model from my app/models dir' unless defined?(EngineModel)
+raise LoadError, 'missing model from my app/models dir' unless defined?(EngineModel)
diff --git a/railties/test/fixtures/public/foo/bar.html b/railties/test/fixtures/public/foo/bar.html
new file mode 100644
index 0000000000..9a35646205
--- /dev/null
+++ b/railties/test/fixtures/public/foo/bar.html
@@ -0,0 +1 @@
+/foo/bar.html \ No newline at end of file
diff --git a/railties/test/fixtures/public/foo/index.html b/railties/test/fixtures/public/foo/index.html
new file mode 100644
index 0000000000..497a2e898f
--- /dev/null
+++ b/railties/test/fixtures/public/foo/index.html
@@ -0,0 +1 @@
+/foo/index.html \ No newline at end of file
diff --git a/railties/test/fixtures/public/index.html b/railties/test/fixtures/public/index.html
new file mode 100644
index 0000000000..525950ba6b
--- /dev/null
+++ b/railties/test/fixtures/public/index.html
@@ -0,0 +1 @@
+/index.html \ No newline at end of file
diff --git a/railties/test/gem_dependency_test.rb b/railties/test/gem_dependency_test.rb
index 9cb02fcd06..189ad02b76 100644
--- a/railties/test/gem_dependency_test.rb
+++ b/railties/test/gem_dependency_test.rb
@@ -46,31 +46,34 @@ class GemDependencyTest < Test::Unit::TestCase
end
def test_gem_adds_load_paths
- @gem.expects(:gem).with(Gem::Dependency.new(@gem.name, nil))
+ @gem.expects(:gem).with(@gem)
@gem.add_load_paths
end
def test_gem_with_version_adds_load_paths
- @gem_with_version.expects(:gem).with(Gem::Dependency.new(@gem_with_version.name, @gem_with_version.requirement.to_s))
+ @gem_with_version.expects(:gem).with(@gem_with_version)
@gem_with_version.add_load_paths
+ assert @gem_with_version.load_paths_added?
end
def test_gem_loading
- @gem.expects(:gem).with(Gem::Dependency.new(@gem.name, nil))
+ @gem.expects(:gem).with(@gem)
@gem.expects(:require).with(@gem.name)
@gem.add_load_paths
@gem.load
+ assert @gem.loaded?
end
def test_gem_with_lib_loading
- @gem_with_lib.expects(:gem).with(Gem::Dependency.new(@gem_with_lib.name, nil))
+ @gem_with_lib.expects(:gem).with(@gem_with_lib)
@gem_with_lib.expects(:require).with(@gem_with_lib.lib)
@gem_with_lib.add_load_paths
@gem_with_lib.load
+ assert @gem_with_lib.loaded?
end
def test_gem_without_lib_loading
- @gem_without_load.expects(:gem).with(Gem::Dependency.new(@gem_without_load.name, nil))
+ @gem_without_load.expects(:gem).with(@gem_without_load)
@gem_without_load.expects(:require).with(@gem_without_load.lib).never
@gem_without_load.add_load_paths
@gem_without_load.load
@@ -132,8 +135,8 @@ class GemDependencyTest < Test::Unit::TestCase
dummy_gem = Rails::GemDependency.new "dummy-gem-g"
dummy_gem.add_load_paths
dummy_gem.load
- assert dummy_gem.loaded?
- assert_equal 2, dummy_gem.dependencies.size
+ assert_equal 1, dummy_gem.dependencies.size
+ assert_equal 1, dummy_gem.dependencies.first.dependencies.size
assert_nothing_raised do
dummy_gem.dependencies.each do |g|
g.dependencies
diff --git a/railties/test/generators/rails_template_runner_test.rb b/railties/test/generators/rails_template_runner_test.rb
index 56afc85eba..2da6bd59b5 100644
--- a/railties/test/generators/rails_template_runner_test.rb
+++ b/railties/test/generators/rails_template_runner_test.rb
@@ -82,6 +82,22 @@ class RailsTemplateRunnerTest < GeneratorTestCase
assert_rails_initializer_includes("config.gem 'mislav-will-paginate', :lib => 'will-paginate', :source => 'http://gems.github.com'")
end
+ def test_gem_with_env_string_should_put_gem_dependency_in_specified_environment
+ run_template_method(:gem, 'rspec', :env => 'test')
+ assert_generated_file_with_data('config/environments/test.rb', "config.gem 'rspec'", 'test')
+ end
+
+ def test_gem_with_env_array_should_put_gem_dependency_in_specified_environments
+ run_template_method(:gem, 'quietbacktrace', :env => %w[ development test ])
+ assert_generated_file_with_data('config/environments/development.rb', "config.gem 'quietbacktrace'")
+ assert_generated_file_with_data('config/environments/test.rb', "config.gem 'quietbacktrace'")
+ end
+
+ def test_gem_with_lib_option_set_to_false_should_put_gem_dependency_in_enviroment_correctly
+ run_template_method(:gem, 'mislav-will-paginate', :lib => false, :source => 'http://gems.github.com')
+ assert_rails_initializer_includes("config.gem 'mislav-will-paginate', :lib => false, :source => 'http://gems.github.com'")
+ end
+
def test_environment_should_include_data_in_environment_initializer_block
load_paths = 'config.load_paths += %w["#{RAILS_ROOT}/app/extras"]'
run_template_method(:environment, load_paths)
diff --git a/railties/test/initializer_test.rb b/railties/test/initializer_test.rb
index eb9ec750da..561f7b8b54 100644
--- a/railties/test/initializer_test.rb
+++ b/railties/test/initializer_test.rb
@@ -1,6 +1,10 @@
require 'abstract_unit'
require 'initializer'
+require 'action_view'
+require 'action_mailer'
+require 'active_record'
+
# Mocks out the configuration
module Rails
def self.configuration
@@ -26,7 +30,6 @@ class Initializer_load_environment_Test < Test::Unit::TestCase
ensure
$initialize_test_set_from_env = nil
end
-
end
class Initializer_eager_loading_Test < Test::Unit::TestCase
@@ -234,7 +237,7 @@ class InitializerPluginLoadingTests < Test::Unit::TestCase
def test_registering_a_plugin_name_that_does_not_exist_raises_a_load_error
only_load_the_following_plugins! [:stubby, :acts_as_a_non_existant_plugin]
- assert_raises(LoadError) do
+ assert_raise(LoadError) do
load_plugins!
end
end
@@ -268,7 +271,6 @@ class InitializerPluginLoadingTests < Test::Unit::TestCase
assert $LOAD_PATH.include?(File.join(plugin_fixture_path('default/acts/acts_as_chunky_bacon'), 'lib'))
end
-
private
def load_plugins!
@@ -288,7 +290,7 @@ class InitializerSetupI18nTests < Test::Unit::TestCase
Dir.stubs(:[]).returns([ "my/test/locale.yml" ])
assert_equal [ "my/test/locale.yml" ], Rails::Configuration.new.i18n.load_path
end
-
+
def test_config_defaults_should_be_added_with_config_settings
File.stubs(:exist?).returns(true)
Dir.stubs(:[]).returns([ "my/test/locale.yml" ])
@@ -298,7 +300,7 @@ class InitializerSetupI18nTests < Test::Unit::TestCase
assert_equal [ "my/test/locale.yml", "my/other/locale.yml" ], config.i18n.load_path
end
-
+
def test_config_defaults_and_settings_should_be_added_to_i18n_defaults
File.stubs(:exist?).returns(true)
Dir.stubs(:[]).returns([ "my/test/locale.yml" ])
@@ -306,17 +308,15 @@ class InitializerSetupI18nTests < Test::Unit::TestCase
config = Rails::Configuration.new
config.i18n.load_path << "my/other/locale.yml"
- # To bring in AV's i18n load path.
- require 'action_view'
-
Rails::Initializer.run(:initialize_i18n, config)
assert_equal [
File.expand_path(File.dirname(__FILE__) + "/../../activesupport/lib/active_support/locale/en.yml"),
File.expand_path(File.dirname(__FILE__) + "/../../actionpack/lib/action_view/locale/en.yml"),
+ File.expand_path(File.dirname(__FILE__) + "/../../activerecord/lib/active_record/locale/en.yml"),
"my/test/locale.yml",
"my/other/locale.yml" ], I18n.load_path.collect { |path| path =~ /^\./ ? File.expand_path(path) : path }
end
-
+
def test_setting_another_default_locale
config = Rails::Configuration.new
config.i18n.default_locale = :de
@@ -325,6 +325,69 @@ class InitializerSetupI18nTests < Test::Unit::TestCase
end
end
+class InitializerDatabaseMiddlewareTest < Test::Unit::TestCase
+ def setup
+ @config = Rails::Configuration.new
+ @config.frameworks = [:active_record, :action_controller, :action_view]
+ end
+
+ def test_initialize_database_middleware_doesnt_perform_anything_when_active_record_not_in_frameworks
+ @config.frameworks.clear
+ @config.expects(:middleware).never
+ Rails::Initializer.run(:initialize_database_middleware, @config)
+ end
+
+ def test_database_middleware_initializes_when_session_store_is_active_record
+ store = ActionController::Base.session_store
+ ActionController::Base.session_store = ActiveRecord::SessionStore
+
+ @config.middleware.expects(:insert_before).with(:"ActiveRecord::SessionStore", ActiveRecord::ConnectionAdapters::ConnectionManagement)
+ @config.middleware.expects(:insert_before).with(:"ActiveRecord::SessionStore", ActiveRecord::QueryCache)
+
+ Rails::Initializer.run(:initialize_database_middleware, @config)
+ ensure
+ ActionController::Base.session_store = store
+ end
+
+ def test_database_middleware_doesnt_initialize_when_session_store_is_not_active_record
+ store = ActionController::Base.session_store
+ ActionController::Base.session_store = ActionController::Session::CookieStore
+
+ # Define the class, so we don't have to actually make it load
+ eval("class ActiveRecord::ConnectionAdapters::ConnectionManagement; end")
+
+ @config.middleware.expects(:use).with(ActiveRecord::ConnectionAdapters::ConnectionManagement)
+ @config.middleware.expects(:use).with(ActiveRecord::QueryCache)
+
+ Rails::Initializer.run(:initialize_database_middleware, @config)
+ ensure
+ ActionController::Base.session_store = store
+ end
+end
+
+class InitializerViewPathsTest < Test::Unit::TestCase
+ def setup
+ @config = Rails::Configuration.new
+ @config.frameworks = [:action_view, :action_controller, :action_mailer]
+
+ ActionController::Base.stubs(:view_paths).returns(stub)
+ ActionMailer::Base.stubs(:view_paths).returns(stub)
+ end
+
+ def test_load_view_paths_doesnt_perform_anything_when_action_view_not_in_frameworks
+ @config.frameworks -= [:action_view]
+ ActionController::Base.view_paths.expects(:load!).never
+ ActionMailer::Base.view_paths.expects(:load!).never
+ Rails::Initializer.run(:load_view_paths, @config)
+ end
+
+ def test_load_view_paths_loads_view_paths
+ ActionController::Base.view_paths.expects(:load!)
+ ActionMailer::Base.view_paths.expects(:load!)
+ Rails::Initializer.run(:load_view_paths, @config)
+ end
+end
+
class RailsRootTest < Test::Unit::TestCase
def test_rails_dot_root_equals_rails_root
assert_equal RAILS_ROOT, Rails.root.to_s
@@ -333,4 +396,4 @@ class RailsRootTest < Test::Unit::TestCase
def test_rails_dot_root_should_be_a_pathname
assert_equal File.join(RAILS_ROOT, 'app', 'controllers'), Rails.root.join('app', 'controllers').to_s
end
-end
+end \ No newline at end of file
diff --git a/railties/test/metal_test.rb b/railties/test/metal_test.rb
new file mode 100644
index 0000000000..d3d231132b
--- /dev/null
+++ b/railties/test/metal_test.rb
@@ -0,0 +1,72 @@
+require 'abstract_unit'
+require 'initializer'
+
+class MetalTest < Test::Unit::TestCase
+ def test_metals_should_return_list_of_found_metal_apps
+ use_appdir("singlemetal") do
+ assert_equal(["FooMetal"], found_metals_as_string_array)
+ end
+ end
+
+ def test_metals_should_respect_class_name_conventions
+ use_appdir("pluralmetal") do
+ assert_equal(["LegacyRoutes"], found_metals_as_string_array)
+ end
+ end
+
+ def test_metals_should_return_alphabetical_list_of_found_metal_apps
+ use_appdir("multiplemetals") do
+ assert_equal(["MetalA", "MetalB"], found_metals_as_string_array)
+ end
+ end
+
+ def test_metals_load_order_should_be_overriden_by_requested_metals
+ use_appdir("multiplemetals") do
+ Rails::Rack::Metal.requested_metals = ["MetalB", "MetalA"]
+ assert_equal(["MetalB", "MetalA"], found_metals_as_string_array)
+ end
+ end
+
+ def test_metals_not_listed_should_not_load
+ use_appdir("multiplemetals") do
+ Rails::Rack::Metal.requested_metals = ["MetalB"]
+ assert_equal(["MetalB"], found_metals_as_string_array)
+ end
+ end
+
+ def test_metal_finding_should_work_with_subfolders
+ use_appdir("subfolders") do
+ assert_equal(["Folder::MetalA", "Folder::MetalB"], found_metals_as_string_array)
+ end
+ end
+
+ def test_metal_finding_with_requested_metals_should_work_with_subfolders
+ use_appdir("subfolders") do
+ Rails::Rack::Metal.requested_metals = ["Folder::MetalB"]
+ assert_equal(["Folder::MetalB"], found_metals_as_string_array)
+ end
+ end
+
+ def test_metal_finding_should_work_with_multiple_metal_paths_in_185_and_below
+ use_appdir("singlemetal") do
+ engine_metal_path = "#{File.dirname(__FILE__)}/fixtures/plugins/engines/engine/app/metal"
+ Rails::Rack::Metal.metal_paths << engine_metal_path
+ $LOAD_PATH << engine_metal_path
+ assert_equal(["FooMetal", "EngineMetal"], found_metals_as_string_array)
+ end
+ end
+
+ private
+
+ def use_appdir(root)
+ dir = "#{File.dirname(__FILE__)}/fixtures/metal/#{root}"
+ Rails::Rack::Metal.metal_paths = ["#{dir}/app/metal"]
+ Rails::Rack::Metal.requested_metals = nil
+ $LOAD_PATH << "#{dir}/app/metal"
+ yield
+ end
+
+ def found_metals_as_string_array
+ Rails::Rack::Metal.metals.map { |m| m.to_s }
+ end
+end
diff --git a/railties/test/plugin_loader_test.rb b/railties/test/plugin_loader_test.rb
index e802b1ace7..b270748dd6 100644
--- a/railties/test/plugin_loader_test.rb
+++ b/railties/test/plugin_loader_test.rb
@@ -120,7 +120,7 @@ class TestPluginLoader < Test::Unit::TestCase
@loader.add_plugin_load_paths
- %w( models controllers helpers ).each do |app_part|
+ %w( models controllers metal helpers ).each do |app_part|
assert ActiveSupport::Dependencies.load_paths.include?(
File.join(plugin_fixture_path('engines/engine'), 'app', app_part)
), "Couldn't find #{app_part} in load path"
@@ -161,4 +161,4 @@ class TestPluginLoader < Test::Unit::TestCase
$LOAD_PATH.clear
ORIGINAL_LOAD_PATH.each { |path| $LOAD_PATH << path }
end
-end \ No newline at end of file
+end
diff --git a/railties/test/plugin_locator_test.rb b/railties/test/plugin_locator_test.rb
index c8404e126e..471d9fc7c3 100644
--- a/railties/test/plugin_locator_test.rb
+++ b/railties/test/plugin_locator_test.rb
@@ -2,7 +2,7 @@ require 'plugin_test_helper'
class PluginLocatorTest < Test::Unit::TestCase
def test_should_require_subclasses_to_implement_the_plugins_method
- assert_raises(RuntimeError) do
+ assert_raise(RuntimeError) do
Rails::Plugin::Locator.new(nil).plugins
end
end
diff --git a/railties/test/plugin_test.rb b/railties/test/plugin_test.rb
index 5500711df8..a6c390a45a 100644
--- a/railties/test/plugin_test.rb
+++ b/railties/test/plugin_test.rb
@@ -52,11 +52,11 @@ class PluginTest < Test::Unit::TestCase
plugin_for(@valid_plugin_path).load_paths
end
- assert_raises(LoadError) do
+ assert_raise(LoadError) do
plugin_for(@empty_plugin_path).load_paths
end
- assert_raises(LoadError) do
+ assert_raise(LoadError) do
plugin_for('this_is_not_a_plugin_directory').load_paths
end
end
@@ -77,13 +77,13 @@ class PluginTest < Test::Unit::TestCase
end
# This is an empty path so it raises
- assert_raises(LoadError) do
+ assert_raise(LoadError) do
plugin = plugin_for(@empty_plugin_path)
plugin.stubs(:evaluate_init_rb)
plugin.send(:load, @initializer)
end
- assert_raises(LoadError) do
+ assert_raise(LoadError) do
plugin = plugin_for('this_is_not_a_plugin_directory')
plugin.stubs(:evaluate_init_rb)
plugin.send(:load, @initializer)
@@ -97,11 +97,11 @@ class PluginTest < Test::Unit::TestCase
end
# This is an empty path so it raises
- assert_raises(LoadError) do
+ assert_raise(LoadError) do
plugin_for(@empty_plugin_path).load_paths
end
- assert_raises(LoadError) do
+ assert_raise(LoadError) do
plugin_for('this_is_not_a_plugin_directory').load_paths
end
end
diff --git a/railties/test/rack_static_test.rb b/railties/test/rack_static_test.rb
new file mode 100644
index 0000000000..ad673f6f19
--- /dev/null
+++ b/railties/test/rack_static_test.rb
@@ -0,0 +1,46 @@
+require 'abstract_unit'
+
+require 'action_controller'
+require 'rails/rack'
+
+class RackStaticTest < ActiveSupport::TestCase
+ def setup
+ FileUtils.cp_r "#{RAILS_ROOT}/fixtures/public", "#{RAILS_ROOT}/public"
+ end
+
+ def teardown
+ FileUtils.rm_rf "#{RAILS_ROOT}/public"
+ end
+
+ DummyApp = lambda { |env|
+ [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]]
+ }
+ App = Rails::Rack::Static.new(DummyApp)
+
+ test "serves dynamic content" do
+ assert_equal "Hello, World!", get("/nofile")
+ end
+
+ test "serves static index at root" do
+ assert_equal "/index.html", get("/index.html")
+ assert_equal "/index.html", get("/index")
+ assert_equal "/index.html", get("/")
+ end
+
+ test "serves static file in directory" do
+ assert_equal "/foo/bar.html", get("/foo/bar.html")
+ assert_equal "/foo/bar.html", get("/foo/bar/")
+ assert_equal "/foo/bar.html", get("/foo/bar")
+ end
+
+ test "serves static index file in directory" do
+ assert_equal "/foo/index.html", get("/foo/index.html")
+ assert_equal "/foo/index.html", get("/foo/")
+ assert_equal "/foo/index.html", get("/foo")
+ end
+
+ private
+ def get(path)
+ Rack::MockRequest.new(App).request("GET", path).body
+ end
+end
diff --git a/railties/test/vendor/gems/dummy-gem-g-1.0.0/.specification b/railties/test/vendor/gems/dummy-gem-g-1.0.0/.specification
index 5483048c1c..27e29912a6 100644
--- a/railties/test/vendor/gems/dummy-gem-g-1.0.0/.specification
+++ b/railties/test/vendor/gems/dummy-gem-g-1.0.0/.specification
@@ -9,7 +9,7 @@ date: 2008-10-03 00:00:00 -04:00
dependencies:
- !ruby/object:Gem::Dependency
name: dummy-gem-f
- type: :development
+ type: :runtime
version_requirement:
version_requirements: !ruby/object:Gem::Requirement
requirements: